diff --git a/package-lock.json b/package-lock.json index 87ad986..02c7d22 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@snipcodeit/mgw", - "version": "0.3.0", + "version": "0.5.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@snipcodeit/mgw", - "version": "0.3.0", + "version": "0.5.1", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -18,7 +18,8 @@ "devDependencies": { "@eslint/js": "^10.0.1", "eslint": "^10.0.2", - "pkgroll": "^2.26.3" + "pkgroll": "^2.26.3", + "vitest": "^2.1.9" }, "engines": { "node": ">=18.0.0" @@ -1211,6 +1212,129 @@ "dev": true, "license": "MIT" }, + "node_modules/@vitest/expect": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.9.tgz", + "integrity": "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.9.tgz", + "integrity": "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.12" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/mocker/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, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/@vitest/pretty-format": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.9.tgz", + "integrity": "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.9.tgz", + "integrity": "sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "2.1.9", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.9.tgz", + "integrity": "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "magic-string": "^0.30.12", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.9.tgz", + "integrity": "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^3.0.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.9.tgz", + "integrity": "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "loupe": "^3.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/acorn": { "version": "8.16.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", @@ -1251,6 +1375,16 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "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, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/astring": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/astring/-/astring-1.9.0.tgz", @@ -1297,6 +1431,43 @@ "node": ">=8" } }, + "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, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "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": ">=18" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, "node_modules/cjs-module-lexer": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.2.0.tgz", @@ -1353,6 +1524,16 @@ } } }, + "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, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -1370,6 +1551,13 @@ "node": ">=0.10.0" } }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, "node_modules/esbuild": { "version": "0.26.0", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.26.0.tgz", @@ -1597,6 +1785,16 @@ "node": ">=0.10.0" } }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -1932,6 +2130,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -2002,6 +2207,25 @@ "dev": true, "license": "MIT" }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "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", @@ -2099,6 +2323,30 @@ "dev": true, "license": "MIT" }, + "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, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, "node_modules/picomatch": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", @@ -2150,6 +2398,35 @@ } } }, + "node_modules/postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "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" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "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", @@ -2357,6 +2634,37 @@ "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, + "license": "ISC" + }, + "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, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "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, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, "node_modules/supports-preserve-symlinks-flag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", @@ -2370,6 +2678,50 @@ "url": "https://github.com/sponsors/ljharb" } }, + "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, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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", @@ -2406,20 +2758,616 @@ "punycode": "^2.1.0" } }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "isexe": "^2.0.0" + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" }, "bin": { - "node-which": "bin/node-which" + "vite": "bin/vite.js" }, "engines": { - "node": ">= 8" + "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.9", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.9.tgz", + "integrity": "sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.7", + "es-module-lexer": "^1.5.4", + "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/vite/node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/vitest": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.9.tgz", + "integrity": "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "2.1.9", + "@vitest/mocker": "2.1.9", + "@vitest/pretty-format": "^2.1.9", + "@vitest/runner": "2.1.9", + "@vitest/snapshot": "2.1.9", + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "debug": "^4.3.7", + "expect-type": "^1.1.0", + "magic-string": "^0.30.12", + "pathe": "^1.1.2", + "std-env": "^3.8.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.1", + "tinypool": "^1.0.1", + "tinyrainbow": "^1.2.0", + "vite": "^5.0.0", + "vite-node": "2.1.9", + "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.9", + "@vitest/ui": "2.1.9", + "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/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "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, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" } }, "node_modules/word-wrap": { diff --git a/package.json b/package.json index 7ff044a..26a50e0 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,9 @@ "scripts": { "build": "pkgroll --clean-dist --src .", "dev": "pkgroll --watch --src .", - "test": "node --test test/*.test.cjs", + "test": "vitest run", + "test:watch": "vitest", + "test:node": "node --test test/*.test.cjs", "lint": "eslint lib/ bin/ test/", "prepublishOnly": "npm run build", "completions": "node bin/generate-completions.cjs", @@ -31,7 +33,8 @@ "devDependencies": { "@eslint/js": "^10.0.1", "eslint": "^10.0.2", - "pkgroll": "^2.26.3" + "pkgroll": "^2.26.3", + "vitest": "^2.1.9" }, "engines": { "node": ">=18.0.0" diff --git a/test/checkpoint.test.js b/test/checkpoint.test.js new file mode 100644 index 0000000..f004919 --- /dev/null +++ b/test/checkpoint.test.js @@ -0,0 +1,821 @@ +/** + * test/checkpoint.test.js — Unit tests for checkpoint read/write and resume + * detection functions in lib/state.cjs. + * + * Covers: + * - updateCheckpoint() merge semantics (step_progress shallow merge, + * artifacts/step_history append-only, resume full-replace) + * - detectCheckpoint() returning null for triage-only checkpoints and + * non-null for checkpoints at plan/execute/verify/pr steps + * - resumeFromCheckpoint() mapping resume.action to resumeStage for all + * documented action values, plus unknown/null default + * - clearCheckpoint() resetting checkpoint to null + * - Forward-compat round-trip: unknown fields in checkpoint are preserved + * + * Isolation strategy: + * - fs.mkdtempSync() creates a real tmp dir per describe block + * - process.cwd() is overridden so getMgwDir() stays sandboxed + * - require.cache is cleared before each require of state.cjs + * - afterEach removes .mgw/ and restores process.cwd() + * - Tmp dirs removed in afterAll via fs.rmSync + * + * This file uses the same isolation pattern as test/state.test.cjs and + * test/validate-and-load.test.js but imports via vitest (ESM) format. + */ + +import { describe, it, expect, beforeEach, afterEach, afterAll } from 'vitest'; +import { createRequire } from 'module'; +import { fileURLToPath } from 'url'; +import path from 'path'; +import fs from 'fs'; +import os from 'os'; + +const _require = createRequire(import.meta.url); +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const REPO_ROOT = path.resolve(__dirname, '..'); +const STATE_MODULE = path.join(REPO_ROOT, 'lib', 'state.cjs'); + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +/** + * Clear state module cache and re-require fresh. + * Required so that process.cwd() overrides take effect on path resolution. + */ +function loadState() { + delete _require.cache[STATE_MODULE]; + return _require(STATE_MODULE); +} + +/** + * Override process.cwd to return tmpDir. + * Returns a restore function — call it in afterEach. + */ +function overrideCwd(tmpDir) { + const original = process.cwd.bind(process); + process.cwd = () => tmpDir; + return () => { + process.cwd = original; + }; +} + +/** + * Remove .mgw/ inside tmpDir if it exists. + */ +function cleanMgw(tmpDir) { + const mgwDir = path.join(tmpDir, '.mgw'); + if (fs.existsSync(mgwDir)) { + fs.rmSync(mgwDir, { recursive: true, force: true }); + } +} + +/** + * Write a minimal issue state file into .mgw/active/. + * Creates directories as needed. + * + * @param {string} tmpDir - Tmp directory root (process.cwd() override target) + * @param {number} issueNumber - Issue number used to name the file + * @param {object} overrides - Fields to merge onto the base state + * @returns {{ filePath: string, state: object }} Written file path and state object + */ +function writeIssueState(tmpDir, issueNumber, overrides = {}) { + const activeDir = path.join(tmpDir, '.mgw', 'active'); + fs.mkdirSync(activeDir, { recursive: true }); + + const base = { + issue_number: issueNumber, + slug: `test-issue-${issueNumber}`, + title: `Test issue ${issueNumber}`, + pipeline_stage: 'triaged', + gsd_route: 'plan-phase', + checkpoint: null, + }; + const state = Object.assign({}, base, overrides); + const fileName = `${issueNumber}-test-issue-${issueNumber}.json`; + const filePath = path.join(activeDir, fileName); + fs.writeFileSync(filePath, JSON.stringify(state, null, 2), 'utf-8'); + return { filePath, state }; +} + +/** + * Read and parse the issue state file from .mgw/active/. + */ +function readIssueState(tmpDir, issueNumber) { + const activeDir = path.join(tmpDir, '.mgw', 'active'); + const entries = fs.readdirSync(activeDir); + const match = entries.find(f => f.startsWith(`${issueNumber}-`) && f.endsWith('.json')); + if (!match) return null; + return JSON.parse(fs.readFileSync(path.join(activeDir, match), 'utf-8')); +} + +// --------------------------------------------------------------------------- +// Group 1: updateCheckpoint() — merge semantics +// --------------------------------------------------------------------------- + +describe('updateCheckpoint() — merge semantics', () => { + let tmpDir; + let restoreCwd; + + beforeEach(() => { + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mgw-cp-test-g1-')); + restoreCwd = overrideCwd(tmpDir); + }); + + afterEach(() => { + restoreCwd(); + cleanMgw(tmpDir); + delete _require.cache[STATE_MODULE]; + }); + + afterAll(() => { + if (fs.existsSync(tmpDir)) { + fs.rmSync(tmpDir, { recursive: true, force: true }); + } + }); + + it('initializes checkpoint from null when none exists', () => { + writeIssueState(tmpDir, 1, { checkpoint: null }); + const { updateCheckpoint } = loadState(); + + const result = updateCheckpoint(1, { pipeline_step: 'plan' }); + + expect(result.updated).toBe(true); + expect(result.checkpoint).toBeTruthy(); + expect(result.checkpoint.pipeline_step).toBe('plan'); + expect(result.checkpoint.schema_version).toBe(1); + expect(result.checkpoint.artifacts).toEqual([]); + expect(result.checkpoint.step_history).toEqual([]); + + const persisted = readIssueState(tmpDir, 1); + expect(persisted.checkpoint).toBeTruthy(); + expect(persisted.checkpoint.pipeline_step).toBe('plan'); + }); + + it('overwrites pipeline_step on subsequent calls', () => { + writeIssueState(tmpDir, 2, { checkpoint: null }); + const { updateCheckpoint } = loadState(); + + updateCheckpoint(2, { pipeline_step: 'plan' }); + updateCheckpoint(2, { pipeline_step: 'execute' }); + + const persisted = readIssueState(tmpDir, 2); + expect(persisted.checkpoint.pipeline_step).toBe('execute'); + }); + + it('shallow-merges step_progress — existing keys preserved, new keys added', () => { + writeIssueState(tmpDir, 3, { checkpoint: null }); + const { updateCheckpoint } = loadState(); + + // First write: sets plan_path and plan_checked + updateCheckpoint(3, { + pipeline_step: 'plan', + step_progress: { plan_path: '/some/plan.md', plan_checked: false }, + }); + + // Second write: only updates plan_checked — plan_path must be preserved + updateCheckpoint(3, { + step_progress: { plan_checked: true }, + }); + + const persisted = readIssueState(tmpDir, 3); + expect(persisted.checkpoint.step_progress.plan_path).toBe('/some/plan.md'); + expect(persisted.checkpoint.step_progress.plan_checked).toBe(true); + }); + + it('appends artifacts — never replaces existing entries', () => { + writeIssueState(tmpDir, 4, { checkpoint: null }); + const { updateCheckpoint } = loadState(); + + const artifact1 = { path: 'plan.md', type: 'plan', created_at: '2026-03-06T10:00:00Z' }; + const artifact2 = { path: 'summary.md', type: 'summary', created_at: '2026-03-06T11:00:00Z' }; + + updateCheckpoint(4, { artifacts: [artifact1] }); + updateCheckpoint(4, { artifacts: [artifact2] }); + + const persisted = readIssueState(tmpDir, 4); + expect(persisted.checkpoint.artifacts).toHaveLength(2); + expect(persisted.checkpoint.artifacts[0].path).toBe('plan.md'); + expect(persisted.checkpoint.artifacts[1].path).toBe('summary.md'); + }); + + it('appends step_history — never replaces existing entries', () => { + writeIssueState(tmpDir, 5, { checkpoint: null }); + const { updateCheckpoint } = loadState(); + + const entry1 = { step: 'plan', completed_at: '2026-03-06T10:00:00Z', agent_type: 'gsd-planner' }; + const entry2 = { step: 'execute', completed_at: '2026-03-06T11:00:00Z', agent_type: 'gsd-executor' }; + + updateCheckpoint(5, { step_history: [entry1] }); + updateCheckpoint(5, { step_history: [entry2] }); + + const persisted = readIssueState(tmpDir, 5); + expect(persisted.checkpoint.step_history).toHaveLength(2); + expect(persisted.checkpoint.step_history[0].step).toBe('plan'); + expect(persisted.checkpoint.step_history[1].step).toBe('execute'); + }); + + it('fully replaces resume on each call (resume.context is opaque)', () => { + writeIssueState(tmpDir, 6, { checkpoint: null }); + const { updateCheckpoint } = loadState(); + + updateCheckpoint(6, { resume: { action: 'spawn-executor', context: { quick_dir: '/a' } } }); + updateCheckpoint(6, { resume: { action: 'spawn-verifier', context: { quick_dir: '/b', plan_num: 2 } } }); + + const persisted = readIssueState(tmpDir, 6); + expect(persisted.checkpoint.resume.action).toBe('spawn-verifier'); + expect(persisted.checkpoint.resume.context.quick_dir).toBe('/b'); + expect(persisted.checkpoint.resume.context.plan_num).toBe(2); + // Old context from first call must not persist + expect(Object.keys(persisted.checkpoint.resume.context)).toHaveLength(2); + }); + + it('updates last_agent_output on each call', () => { + writeIssueState(tmpDir, 7, { checkpoint: null }); + const { updateCheckpoint } = loadState(); + + updateCheckpoint(7, { last_agent_output: '/first/output.md' }); + updateCheckpoint(7, { last_agent_output: '/second/output.md' }); + + const persisted = readIssueState(tmpDir, 7); + expect(persisted.checkpoint.last_agent_output).toBe('/second/output.md'); + }); + + it('always updates updated_at timestamp', () => { + writeIssueState(tmpDir, 8, { checkpoint: null }); + const { updateCheckpoint } = loadState(); + + const before = new Date().toISOString(); + const result = updateCheckpoint(8, { pipeline_step: 'plan' }); + + expect(result.checkpoint.updated_at).toBeDefined(); + expect(result.checkpoint.updated_at >= before).toBe(true); + }); + + it('throws when no state file exists for the issue number', () => { + // Do not create a state file for issue 9 + const activeDir = path.join(tmpDir, '.mgw', 'active'); + fs.mkdirSync(activeDir, { recursive: true }); + const { updateCheckpoint } = loadState(); + + expect(() => updateCheckpoint(9, { pipeline_step: 'plan' })).toThrow(/No state file found/); + }); +}); + +// --------------------------------------------------------------------------- +// Group 2: detectCheckpoint() — null-return semantics +// --------------------------------------------------------------------------- + +describe('detectCheckpoint() — null-return semantics', () => { + let tmpDir; + let restoreCwd; + + beforeEach(() => { + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mgw-cp-test-g2-')); + restoreCwd = overrideCwd(tmpDir); + }); + + afterEach(() => { + restoreCwd(); + cleanMgw(tmpDir); + delete _require.cache[STATE_MODULE]; + }); + + afterAll(() => { + if (fs.existsSync(tmpDir)) { + fs.rmSync(tmpDir, { recursive: true, force: true }); + } + }); + + it('returns null when no state file exists for the issue number', () => { + const activeDir = path.join(tmpDir, '.mgw', 'active'); + fs.mkdirSync(activeDir, { recursive: true }); + const { detectCheckpoint } = loadState(); + + expect(detectCheckpoint(100)).toBeNull(); + }); + + it('returns null when checkpoint field is null', () => { + writeIssueState(tmpDir, 101, { checkpoint: null }); + const { detectCheckpoint } = loadState(); + + expect(detectCheckpoint(101)).toBeNull(); + }); + + it('returns null when pipeline_step is "triage" (index 0 — not resumable)', () => { + writeIssueState(tmpDir, 102, { + checkpoint: { + schema_version: 1, + pipeline_step: 'triage', + step_progress: { comment_check_done: true }, + last_agent_output: null, + artifacts: [], + resume: { action: 'begin-execution', context: {} }, + started_at: '2026-03-06T10:00:00Z', + updated_at: '2026-03-06T10:01:00Z', + step_history: [], + }, + }); + const { detectCheckpoint } = loadState(); + + expect(detectCheckpoint(102)).toBeNull(); + }); + + it('returns checkpoint data when pipeline_step is "plan" (index 1)', () => { + writeIssueState(tmpDir, 103, { + checkpoint: { + schema_version: 1, + pipeline_step: 'plan', + step_progress: { plan_path: '/plan.md', plan_checked: false }, + last_agent_output: '/plan.md', + artifacts: [{ path: '/plan.md', type: 'plan', created_at: '2026-03-06T10:00:00Z' }], + resume: { action: 'run-plan-checker', context: { quick_dir: '/q' } }, + started_at: '2026-03-06T10:00:00Z', + updated_at: '2026-03-06T10:05:00Z', + step_history: [], + }, + }); + const { detectCheckpoint } = loadState(); + + const cp = detectCheckpoint(103); + expect(cp).not.toBeNull(); + expect(cp.pipeline_step).toBe('plan'); + expect(cp.step_progress.plan_path).toBe('/plan.md'); + expect(cp.artifacts).toHaveLength(1); + expect(cp.resume.action).toBe('run-plan-checker'); + }); + + it('returns checkpoint data when pipeline_step is "execute"', () => { + writeIssueState(tmpDir, 104, { + checkpoint: { + schema_version: 1, + pipeline_step: 'execute', + step_progress: { gsd_phase: 1, tasks_completed: 2, tasks_total: 5 }, + last_agent_output: null, + artifacts: [], + resume: { action: 'continue-execution', context: { phase_number: 1 } }, + started_at: '2026-03-06T10:00:00Z', + updated_at: '2026-03-06T10:10:00Z', + step_history: [], + }, + }); + const { detectCheckpoint } = loadState(); + + const cp = detectCheckpoint(104); + expect(cp).not.toBeNull(); + expect(cp.pipeline_step).toBe('execute'); + }); + + it('returns checkpoint data when pipeline_step is "verify"', () => { + writeIssueState(tmpDir, 105, { + checkpoint: { + schema_version: 1, + pipeline_step: 'verify', + step_progress: { verification_path: '/verify.md', must_haves_checked: true }, + last_agent_output: '/verify.md', + artifacts: [], + resume: { action: 'create-pr', context: {} }, + started_at: '2026-03-06T10:00:00Z', + updated_at: '2026-03-06T10:20:00Z', + step_history: [], + }, + }); + const { detectCheckpoint } = loadState(); + + const cp = detectCheckpoint(105); + expect(cp).not.toBeNull(); + expect(cp.pipeline_step).toBe('verify'); + }); + + it('returns checkpoint data when pipeline_step is "pr"', () => { + writeIssueState(tmpDir, 106, { + checkpoint: { + schema_version: 1, + pipeline_step: 'pr', + step_progress: { branch_pushed: true, pr_number: 42, pr_url: 'https://github.com/r/p/pulls/42' }, + last_agent_output: 'https://github.com/r/p/pulls/42', + artifacts: [], + resume: { action: 'cleanup', context: { pr_number: 42 } }, + started_at: '2026-03-06T10:00:00Z', + updated_at: '2026-03-06T10:30:00Z', + step_history: [], + }, + }); + const { detectCheckpoint } = loadState(); + + const cp = detectCheckpoint(106); + expect(cp).not.toBeNull(); + expect(cp.pipeline_step).toBe('pr'); + }); +}); + +// --------------------------------------------------------------------------- +// Group 3: resumeFromCheckpoint() — action → stage mapping +// --------------------------------------------------------------------------- + +describe('resumeFromCheckpoint() — action to resumeStage mapping', () => { + let tmpDir; + let restoreCwd; + + beforeEach(() => { + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mgw-cp-test-g3-')); + restoreCwd = overrideCwd(tmpDir); + }); + + afterEach(() => { + restoreCwd(); + cleanMgw(tmpDir); + delete _require.cache[STATE_MODULE]; + }); + + afterAll(() => { + if (fs.existsSync(tmpDir)) { + fs.rmSync(tmpDir, { recursive: true, force: true }); + } + }); + + /** + * Helper: write an issue state with a plan-step checkpoint and a given resume action. + */ + function writeCheckpointWithAction(issueNumber, action, extraHistory = []) { + writeIssueState(tmpDir, issueNumber, { + checkpoint: { + schema_version: 1, + pipeline_step: 'plan', + step_progress: {}, + last_agent_output: null, + artifacts: [], + resume: { action, context: {} }, + started_at: '2026-03-06T10:00:00Z', + updated_at: '2026-03-06T10:05:00Z', + step_history: extraHistory, + }, + }); + } + + it('returns null when no resumable checkpoint exists (triage-only)', () => { + writeIssueState(tmpDir, 200, { + checkpoint: { + schema_version: 1, + pipeline_step: 'triage', + step_progress: {}, + last_agent_output: null, + artifacts: [], + resume: { action: 'begin-execution', context: {} }, + started_at: '2026-03-06T10:00:00Z', + updated_at: '2026-03-06T10:00:00Z', + step_history: [], + }, + }); + const { resumeFromCheckpoint } = loadState(); + + expect(resumeFromCheckpoint(200)).toBeNull(); + }); + + it('maps "run-plan-checker" → resumeStage "planning"', () => { + writeCheckpointWithAction(201, 'run-plan-checker'); + const { resumeFromCheckpoint } = loadState(); + + const result = resumeFromCheckpoint(201); + expect(result).not.toBeNull(); + expect(result.resumeStage).toBe('planning'); + expect(result.resumeAction).toBe('run-plan-checker'); + }); + + it('maps "spawn-executor" → resumeStage "executing"', () => { + writeCheckpointWithAction(202, 'spawn-executor'); + const { resumeFromCheckpoint } = loadState(); + + const result = resumeFromCheckpoint(202); + expect(result.resumeStage).toBe('executing'); + expect(result.resumeAction).toBe('spawn-executor'); + }); + + it('maps "continue-execution" → resumeStage "executing"', () => { + writeCheckpointWithAction(203, 'continue-execution'); + const { resumeFromCheckpoint } = loadState(); + + const result = resumeFromCheckpoint(203); + expect(result.resumeStage).toBe('executing'); + expect(result.resumeAction).toBe('continue-execution'); + }); + + it('maps "spawn-verifier" → resumeStage "verifying"', () => { + writeCheckpointWithAction(204, 'spawn-verifier'); + const { resumeFromCheckpoint } = loadState(); + + const result = resumeFromCheckpoint(204); + expect(result.resumeStage).toBe('verifying'); + expect(result.resumeAction).toBe('spawn-verifier'); + }); + + it('maps "create-pr" → resumeStage "pr-pending"', () => { + writeCheckpointWithAction(205, 'create-pr'); + const { resumeFromCheckpoint } = loadState(); + + const result = resumeFromCheckpoint(205); + expect(result.resumeStage).toBe('pr-pending'); + expect(result.resumeAction).toBe('create-pr'); + }); + + it('maps "begin-execution" → resumeStage "planning"', () => { + writeCheckpointWithAction(206, 'begin-execution'); + const { resumeFromCheckpoint } = loadState(); + + const result = resumeFromCheckpoint(206); + expect(result.resumeStage).toBe('planning'); + expect(result.resumeAction).toBe('begin-execution'); + }); + + it('maps null action → resumeStage "planning" (safe default), resumeAction "unknown"', () => { + writeCheckpointWithAction(207, null); + const { resumeFromCheckpoint } = loadState(); + + const result = resumeFromCheckpoint(207); + expect(result.resumeStage).toBe('planning'); + expect(result.resumeAction).toBe('unknown'); + }); + + it('maps unrecognized action → resumeStage "planning" (safe default)', () => { + writeCheckpointWithAction(208, 'future-unknown-action'); + const { resumeFromCheckpoint } = loadState(); + + const result = resumeFromCheckpoint(208); + expect(result.resumeStage).toBe('planning'); + expect(result.resumeAction).toBe('future-unknown-action'); + }); + + it('derives completedSteps from step_history entries', () => { + const history = [ + { step: 'plan', completed_at: '2026-03-06T10:00:00Z', agent_type: 'gsd-planner' }, + { step: 'execute', completed_at: '2026-03-06T10:30:00Z', agent_type: 'gsd-executor' }, + ]; + writeCheckpointWithAction(209, 'spawn-verifier', history); + const { resumeFromCheckpoint } = loadState(); + + const result = resumeFromCheckpoint(209); + expect(result.completedSteps).toEqual(['plan', 'execute']); + }); + + it('returns empty completedSteps when step_history is empty', () => { + writeCheckpointWithAction(210, 'run-plan-checker', []); + const { resumeFromCheckpoint } = loadState(); + + const result = resumeFromCheckpoint(210); + expect(result.completedSteps).toEqual([]); + }); + + it('returns checkpoint data nested under result.checkpoint', () => { + writeCheckpointWithAction(211, 'spawn-executor'); + const { resumeFromCheckpoint } = loadState(); + + const result = resumeFromCheckpoint(211); + expect(result.checkpoint).toBeDefined(); + expect(result.checkpoint.pipeline_step).toBe('plan'); + expect(result.checkpoint.resume.action).toBe('spawn-executor'); + }); +}); + +// --------------------------------------------------------------------------- +// Group 4: clearCheckpoint() — reset behavior +// --------------------------------------------------------------------------- + +describe('clearCheckpoint() — reset to null', () => { + let tmpDir; + let restoreCwd; + + beforeEach(() => { + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mgw-cp-test-g4-')); + restoreCwd = overrideCwd(tmpDir); + }); + + afterEach(() => { + restoreCwd(); + cleanMgw(tmpDir); + delete _require.cache[STATE_MODULE]; + }); + + afterAll(() => { + if (fs.existsSync(tmpDir)) { + fs.rmSync(tmpDir, { recursive: true, force: true }); + } + }); + + it('sets checkpoint to null and returns { cleared: true } when checkpoint was non-null', () => { + writeIssueState(tmpDir, 300, { + checkpoint: { + schema_version: 1, + pipeline_step: 'plan', + step_progress: {}, + last_agent_output: null, + artifacts: [], + resume: { action: 'spawn-executor', context: {} }, + started_at: '2026-03-06T10:00:00Z', + updated_at: '2026-03-06T10:05:00Z', + step_history: [], + }, + }); + const { clearCheckpoint } = loadState(); + + const result = clearCheckpoint(300); + + expect(result).toEqual({ cleared: true }); + + const persisted = readIssueState(tmpDir, 300); + expect(persisted.checkpoint).toBeNull(); + }); + + it('returns { cleared: false } when checkpoint was already null', () => { + writeIssueState(tmpDir, 301, { checkpoint: null }); + const { clearCheckpoint } = loadState(); + + const result = clearCheckpoint(301); + + expect(result).toEqual({ cleared: false }); + + const persisted = readIssueState(tmpDir, 301); + expect(persisted.checkpoint).toBeNull(); + }); + + it('preserves other fields in the state file (pipeline_stage, triage, etc.)', () => { + writeIssueState(tmpDir, 302, { + pipeline_stage: 'executing', + gsd_route: 'plan-phase', + checkpoint: { + schema_version: 1, + pipeline_step: 'execute', + step_progress: {}, + last_agent_output: null, + artifacts: [], + resume: { action: null, context: {} }, + started_at: '2026-03-06T10:00:00Z', + updated_at: '2026-03-06T10:05:00Z', + step_history: [], + }, + }); + const { clearCheckpoint } = loadState(); + + clearCheckpoint(302); + + const persisted = readIssueState(tmpDir, 302); + expect(persisted.pipeline_stage).toBe('executing'); + expect(persisted.gsd_route).toBe('plan-phase'); + expect(persisted.checkpoint).toBeNull(); + }); + + it('writes atomically (uses atomicWriteJson — no .tmp file left behind)', () => { + writeIssueState(tmpDir, 303, { + checkpoint: { + schema_version: 1, + pipeline_step: 'plan', + step_progress: {}, + last_agent_output: null, + artifacts: [], + resume: { action: null, context: {} }, + started_at: '2026-03-06T10:00:00Z', + updated_at: '2026-03-06T10:05:00Z', + step_history: [], + }, + }); + const { clearCheckpoint } = loadState(); + + clearCheckpoint(303); + + const activeDir = path.join(tmpDir, '.mgw', 'active'); + const entries = fs.readdirSync(activeDir); + const tmpFiles = entries.filter(f => f.endsWith('.tmp')); + expect(tmpFiles).toHaveLength(0); + }); + + it('throws when no state file found for the issue number', () => { + const activeDir = path.join(tmpDir, '.mgw', 'active'); + fs.mkdirSync(activeDir, { recursive: true }); + const { clearCheckpoint } = loadState(); + + expect(() => clearCheckpoint(999)).toThrow(/No state file found/); + }); +}); + +// --------------------------------------------------------------------------- +// Group 5: Forward-compatibility — unknown fields preserved on round-trip +// --------------------------------------------------------------------------- + +describe('Forward-compatibility — unknown fields preserved on round-trip', () => { + let tmpDir; + let restoreCwd; + + beforeEach(() => { + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mgw-cp-test-g5-')); + restoreCwd = overrideCwd(tmpDir); + }); + + afterEach(() => { + restoreCwd(); + cleanMgw(tmpDir); + delete _require.cache[STATE_MODULE]; + }); + + afterAll(() => { + if (fs.existsSync(tmpDir)) { + fs.rmSync(tmpDir, { recursive: true, force: true }); + } + }); + + it('preserves unknown top-level checkpoint fields on updateCheckpoint round-trip', () => { + // Simulate a checkpoint written by a future version with an extra field + writeIssueState(tmpDir, 400, { + checkpoint: { + schema_version: 1, + pipeline_step: 'plan', + step_progress: { plan_path: '/plan.md' }, + last_agent_output: '/plan.md', + artifacts: [], + resume: { action: 'spawn-executor', context: {} }, + started_at: '2026-03-06T10:00:00Z', + updated_at: '2026-03-06T10:05:00Z', + step_history: [], + // Future field that current consumers do not know about + future_field: 'preserve-me', + another_future_field: { nested: true }, + }, + }); + + const { updateCheckpoint } = loadState(); + + // Perform a read-modify-write (update step_progress) + updateCheckpoint(400, { + step_progress: { plan_checked: true }, + }); + + const persisted = readIssueState(tmpDir, 400); + + // Known fields work correctly + expect(persisted.checkpoint.step_progress.plan_path).toBe('/plan.md'); + expect(persisted.checkpoint.step_progress.plan_checked).toBe(true); + + // Unknown fields must be preserved + expect(persisted.checkpoint.future_field).toBe('preserve-me'); + expect(persisted.checkpoint.another_future_field).toEqual({ nested: true }); + }); + + it('preserves unknown step_progress keys on shallow merge', () => { + writeIssueState(tmpDir, 401, { + checkpoint: { + schema_version: 1, + pipeline_step: 'execute', + step_progress: { + gsd_phase: 1, + tasks_completed: 2, + tasks_total: 5, + // Key from a future pipeline version + future_progress_key: 'do-not-lose-me', + }, + last_agent_output: null, + artifacts: [], + resume: { action: 'continue-execution', context: {} }, + started_at: '2026-03-06T10:00:00Z', + updated_at: '2026-03-06T10:10:00Z', + step_history: [], + }, + }); + + const { updateCheckpoint } = loadState(); + + // Update tasks_completed only — future_progress_key must survive + updateCheckpoint(401, { + step_progress: { tasks_completed: 3 }, + }); + + const persisted = readIssueState(tmpDir, 401); + expect(persisted.checkpoint.step_progress.gsd_phase).toBe(1); + expect(persisted.checkpoint.step_progress.tasks_completed).toBe(3); + expect(persisted.checkpoint.step_progress.tasks_total).toBe(5); + expect(persisted.checkpoint.step_progress.future_progress_key).toBe('do-not-lose-me'); + }); + + it('detectCheckpoint returns unknown step_progress keys intact', () => { + writeIssueState(tmpDir, 402, { + checkpoint: { + schema_version: 1, + pipeline_step: 'plan', + step_progress: { + plan_path: '/plan.md', + unknown_future_key: 42, + }, + last_agent_output: null, + artifacts: [], + resume: { action: 'run-plan-checker', context: {} }, + started_at: '2026-03-06T10:00:00Z', + updated_at: '2026-03-06T10:05:00Z', + step_history: [], + }, + }); + + const { detectCheckpoint } = loadState(); + const cp = detectCheckpoint(402); + + expect(cp).not.toBeNull(); + expect(cp.step_progress.plan_path).toBe('/plan.md'); + expect(cp.step_progress.unknown_future_key).toBe(42); + }); +}); diff --git a/test/setup.js b/test/setup.js new file mode 100644 index 0000000..3034cb5 --- /dev/null +++ b/test/setup.js @@ -0,0 +1,88 @@ +/** + * test/setup.js — Vitest global setup + * + * Auto-activates mock-github and mock-gsd-agent before each test, and + * deactivates them after. Both mocks are conditionally required — the + * setup works correctly even when lib/mock-github.cjs or + * lib/mock-gsd-agent.cjs are not yet present (e.g., when PRs #258 and + * #259 are not yet merged to main). + * + * To use mocks in a vitest test file: + * + * import { mockGitHub, mockGsdAgent } from './setup.js'; + * + * test('my test', () => { + * // mocks are already active (activated in beforeEach) + * mockGitHub.setResponse('gh issue view', '{"number":999}'); + * // ... + * }); + * + * To use a scenario: + * + * import { mockGitHub } from './setup.js'; + * + * beforeEach(() => { + * // Override the global beforeEach activation with a scenario + * mockGitHub.deactivate(); + * mockGitHub.activate('pr-error'); + * }); + */ + +import { beforeEach, afterEach } from 'vitest'; +import { createRequire } from 'module'; +import { fileURLToPath } from 'url'; +import path from 'path'; + +const require = createRequire(import.meta.url); +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const repoRoot = path.resolve(__dirname, '..'); + +// --------------------------------------------------------------------------- +// Conditional mock loading +// --------------------------------------------------------------------------- + +// Conditionally load mock-github — gracefully skip if not present +// (lib/mock-github.cjs lands via PR #258) +let mockGitHub = null; +try { + mockGitHub = require(path.join(repoRoot, 'lib', 'mock-github.cjs')); +} catch (_e) { + // mock-github.cjs not available — tests run without GitHub API interception +} + +// Conditionally load mock-gsd-agent — gracefully skip if not present +// (lib/mock-gsd-agent.cjs lands via PR #259) +let mockGsdAgent = null; +try { + mockGsdAgent = require(path.join(repoRoot, 'lib', 'mock-gsd-agent.cjs')); +} catch (_e) { + // mock-gsd-agent.cjs not available — tests run without agent spawn interception +} + +// --------------------------------------------------------------------------- +// Auto-activate hooks +// --------------------------------------------------------------------------- + +beforeEach(() => { + if (mockGitHub && typeof mockGitHub.activate === 'function') { + mockGitHub.activate(); + } + if (mockGsdAgent && typeof mockGsdAgent.activate === 'function') { + mockGsdAgent.activate(); + } +}); + +afterEach(() => { + if (mockGitHub && typeof mockGitHub.deactivate === 'function') { + mockGitHub.deactivate(); + } + if (mockGsdAgent && typeof mockGsdAgent.deactivate === 'function') { + mockGsdAgent.deactivate(); + } +}); + +// --------------------------------------------------------------------------- +// Exports — available for test files that need direct mock access +// --------------------------------------------------------------------------- + +export { mockGitHub, mockGsdAgent }; diff --git a/vitest.config.js b/vitest.config.js new file mode 100644 index 0000000..0cbcbb9 --- /dev/null +++ b/vitest.config.js @@ -0,0 +1,12 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + environment: 'node', + setupFiles: ['./test/setup.js'], + // Target .test.js and .spec.js files for vitest + // Exclude .test.cjs files — those use node:test (run via npm run test:node) + include: ['test/**/*.{test,spec}.{js,mjs}'], + exclude: ['test/**/*.test.cjs'], + }, +});