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 09650a5..26a50e0 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "@eslint/js": "^10.0.1", "eslint": "^10.0.2", "pkgroll": "^2.26.3", - "vitest": "^2.0.0" + "vitest": "^2.1.9" }, "engines": { "node": ">=18.0.0" diff --git a/test/validate-and-load.test.js b/test/validate-and-load.test.js new file mode 100644 index 0000000..e3c6825 --- /dev/null +++ b/test/validate-and-load.test.js @@ -0,0 +1,669 @@ +/** + * test/validate-and-load.test.js — Integration tests for validate_and_load + * and state file lifecycle in lib/state.cjs. + * + * Covers: + * - Fresh .mgw/ init (directories, gitignore injection, cross-refs.json) + * - migrateProjectState() idempotency + * - loadActiveIssue() lifecycle (prefix pattern, null returns) + * - Staleness detection with mocked GitHub updatedAt timestamps + * - loadCrossRefs() validation and warning generation + * + * Isolation strategy: + * - fs.mkdtempSync() creates a real tmp dir per describe block + * - process.cwd() is overridden to point at the tmp dir so getMgwDir() + * stays sandboxed — same pattern as test/state.test.cjs + * - require.cache is cleared before each require of state.cjs so the + * cwd override takes effect in the module's top-level path resolution + * - afterEach removes .mgw/ inside tmp dir and restores process.cwd() + * - Tmp dirs removed in afterAll via fs.rmSync + * - mock-github.cjs loaded conditionally — staleness tests skip gracefully + * when the mock is absent (PR #247 not yet merged to this branch) + */ + +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'; +// Do NOT import execSync directly — the mock patches child_process.execSync on +// the module object, so we must call it via _require('child_process').execSync +// to pick up the patched version after mock.activate(). + +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'); +const MOCK_GITHUB_MODULE = path.join(REPO_ROOT, 'lib', 'mock-github.cjs'); + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +/** + * Clear the state module cache and re-require it 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 project.json into the tmp dir's .mgw/ directory. + * Creates .mgw/ if it does not exist. + */ +function writeMinimalProject(tmpDir, overrides = {}) { + const mgwDir = path.join(tmpDir, '.mgw'); + fs.mkdirSync(mgwDir, { recursive: true }); + const base = { milestones: [], current_milestone: 1 }; + const data = Object.assign({}, base, overrides); + fs.writeFileSync(path.join(mgwDir, 'project.json'), JSON.stringify(data, null, 2), 'utf-8'); + return data; +} + +/** + * Conditionally load mock-github.cjs. + * Returns the module if available, null otherwise. + */ +function tryLoadMockGitHub() { + try { + delete _require.cache[MOCK_GITHUB_MODULE]; + return _require(MOCK_GITHUB_MODULE); + } catch { + return null; + } +} + +// --------------------------------------------------------------------------- +// Group 1: Fresh .mgw/ init +// --------------------------------------------------------------------------- + +describe('Group 1: Fresh .mgw/ init', () => { + let tmpDir; + let restoreCwd; + + beforeEach(() => { + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mgw-validate-g1-')); + restoreCwd = overrideCwd(tmpDir); + }); + + afterEach(() => { + restoreCwd(); + cleanMgw(tmpDir); + delete _require.cache[STATE_MODULE]; + }); + + afterAll(() => { + if (tmpDir && fs.existsSync(tmpDir)) { + fs.rmSync(tmpDir, { recursive: true, force: true }); + } + }); + + it('T1.1 – getMgwDir returns .mgw/ inside tmp dir', () => { + const { getMgwDir } = loadState(); + expect(getMgwDir()).toBe(path.join(tmpDir, '.mgw')); + }); + + it('T1.2 – loadProjectState returns null when .mgw/ is absent', () => { + const { loadProjectState } = loadState(); + expect(loadProjectState()).toBeNull(); + }); + + it('T1.3 – writeProjectState creates .mgw/ and writes project.json', () => { + const { writeProjectState, loadProjectState } = loadState(); + const state = { milestones: [], active_gsd_milestone: null }; + writeProjectState(state); + + const projectPath = path.join(tmpDir, '.mgw', 'project.json'); + expect(fs.existsSync(projectPath)).toBe(true); + + const loaded = loadProjectState(); + expect(loaded).toEqual(state); + }); + + it('T1.4 – cross-refs.json creation via storeDependencies', () => { + const { storeDependencies } = loadState(); + + // .mgw/ does not yet exist — storeDependencies should create it + const result = storeDependencies(99, [42]); + expect(result.added).toBe(1); + expect(result.existing).toBe(0); + + const crossRefsPath = path.join(tmpDir, '.mgw', 'cross-refs.json'); + expect(fs.existsSync(crossRefsPath)).toBe(true); + + const crossRefs = JSON.parse(fs.readFileSync(crossRefsPath, 'utf-8')); + expect(Array.isArray(crossRefs.links)).toBe(true); + expect(crossRefs.links).toHaveLength(1); + expect(crossRefs.links[0]).toMatchObject({ + a: '#99', + b: '#42', + type: 'blocked-by', + }); + }); + + it('T1.5 – gitignore injection pattern is idempotent', () => { + // Simulate the init process: inject .mgw/ and .worktrees/ into .gitignore + const gitignorePath = path.join(tmpDir, '.gitignore'); + + // First injection + function injectIfAbsent(entry) { + const current = fs.existsSync(gitignorePath) + ? fs.readFileSync(gitignorePath, 'utf-8') + : ''; + if (!current.split('\n').some(line => line.trim() === entry)) { + fs.appendFileSync(gitignorePath, (current.length > 0 && !current.endsWith('\n') ? '\n' : '') + entry + '\n', 'utf-8'); + } + } + + injectIfAbsent('.mgw/'); + injectIfAbsent('.worktrees/'); + + const afterFirst = fs.readFileSync(gitignorePath, 'utf-8'); + const firstMgwCount = afterFirst.split('\n').filter(l => l.trim() === '.mgw/').length; + const firstWtCount = afterFirst.split('\n').filter(l => l.trim() === '.worktrees/').length; + expect(firstMgwCount).toBe(1); + expect(firstWtCount).toBe(1); + + // Second injection (idempotency) + injectIfAbsent('.mgw/'); + injectIfAbsent('.worktrees/'); + + const afterSecond = fs.readFileSync(gitignorePath, 'utf-8'); + const secondMgwCount = afterSecond.split('\n').filter(l => l.trim() === '.mgw/').length; + const secondWtCount = afterSecond.split('\n').filter(l => l.trim() === '.worktrees/').length; + expect(secondMgwCount).toBe(1); + expect(secondWtCount).toBe(1); + }); +}); + +// --------------------------------------------------------------------------- +// Group 2: migrateProjectState() idempotency +// --------------------------------------------------------------------------- + +describe('Group 2: migrateProjectState() idempotency', () => { + let tmpDir; + let restoreCwd; + + beforeEach(() => { + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mgw-validate-g2-')); + restoreCwd = overrideCwd(tmpDir); + }); + + afterEach(() => { + restoreCwd(); + cleanMgw(tmpDir); + delete _require.cache[STATE_MODULE]; + }); + + afterAll(() => { + if (tmpDir && fs.existsSync(tmpDir)) { + fs.rmSync(tmpDir, { recursive: true, force: true }); + } + }); + + it('T2.1 – migrate adds active_gsd_milestone when absent', () => { + writeMinimalProject(tmpDir, { milestones: [] }); + // Confirm field is absent before migration + const raw = JSON.parse(fs.readFileSync(path.join(tmpDir, '.mgw', 'project.json'), 'utf-8')); + expect(raw.active_gsd_milestone).toBeUndefined(); + + const { migrateProjectState, loadProjectState } = loadState(); + migrateProjectState(); + + const migrated = loadProjectState(); + expect(migrated).not.toBeNull(); + expect(migrated.active_gsd_milestone).toBeNull(); + }); + + it('T2.2 – migrate is idempotent when called twice', () => { + writeMinimalProject(tmpDir, { milestones: [{ title: 'v1', gsd_milestone_id: 'v1.0' }] }); + const { migrateProjectState, loadProjectState } = loadState(); + + migrateProjectState(); + const afterFirst = loadProjectState(); + + // Clear cache to simulate a fresh call context + delete _require.cache[STATE_MODULE]; + const { migrateProjectState: migrate2, loadProjectState: load2 } = loadState(); + migrate2(); + const afterSecond = load2(); + + // Milestone count must not change + expect(afterSecond.milestones.length).toBe(afterFirst.milestones.length); + // Fields must still be present once + expect(afterSecond.active_gsd_milestone).toBeNull(); + expect(typeof afterSecond.milestones[0].gsd_milestone_id).toBe('string'); + }); + + it('T2.3 – migrate adds gsd_milestone_id, gsd_state, roadmap_archived_at to milestones', () => { + // Write milestone without the migration fields + writeMinimalProject(tmpDir, { + milestones: [{ title: 'v1', number: 1 }], + }); + + const { migrateProjectState, loadProjectState } = loadState(); + migrateProjectState(); + + const state = loadProjectState(); + const m = state.milestones[0]; + expect(m).toHaveProperty('gsd_milestone_id'); + expect(m.gsd_milestone_id).toBeNull(); + expect(m).toHaveProperty('gsd_state'); + expect(m.gsd_state).toBeNull(); + expect(m).toHaveProperty('roadmap_archived_at'); + expect(m.roadmap_archived_at).toBeNull(); + }); + + it('T2.4 – migrate adds retry_count, dead_letter, checkpoint to active issue files', () => { + // Create .mgw/active/ with a minimal issue file missing retry fields + const activeDir = path.join(tmpDir, '.mgw', 'active'); + fs.mkdirSync(activeDir, { recursive: true }); + + const issueFile = path.join(activeDir, '42-some-issue.json'); + fs.writeFileSync(issueFile, JSON.stringify({ issue_number: 42, title: 'Some issue', pipeline_stage: 'triaged' }), 'utf-8'); + + // Write project.json too so migration can complete + writeMinimalProject(tmpDir, { milestones: [] }); + + const { migrateProjectState } = loadState(); + migrateProjectState(); + + const migrated = JSON.parse(fs.readFileSync(issueFile, 'utf-8')); + expect(migrated.retry_count).toBe(0); + expect(migrated.dead_letter).toBe(false); + expect(migrated.checkpoint).toBeNull(); + }); + + it('T2.5 – migrate is idempotent on active files (run twice)', () => { + // Create active file that already HAS retry fields + const activeDir = path.join(tmpDir, '.mgw', 'active'); + fs.mkdirSync(activeDir, { recursive: true }); + + const issueFile = path.join(activeDir, '55-existing.json'); + const existing = { + issue_number: 55, + title: 'Existing issue', + pipeline_stage: 'triaged', + retry_count: 3, + dead_letter: true, + checkpoint: null, + }; + fs.writeFileSync(issueFile, JSON.stringify(existing), 'utf-8'); + writeMinimalProject(tmpDir, { milestones: [] }); + + const { migrateProjectState } = loadState(); + migrateProjectState(); + + delete _require.cache[STATE_MODULE]; + const { migrateProjectState: m2 } = loadState(); + m2(); + + const afterTwo = JSON.parse(fs.readFileSync(issueFile, 'utf-8')); + // Migration must NOT overwrite existing non-default values + expect(afterTwo.retry_count).toBe(3); + expect(afterTwo.dead_letter).toBe(true); + expect(afterTwo.checkpoint).toBeNull(); + }); +}); + +// --------------------------------------------------------------------------- +// Group 3: loadActiveIssue lifecycle +// --------------------------------------------------------------------------- + +describe('Group 3: loadActiveIssue lifecycle', () => { + let tmpDir; + let restoreCwd; + + beforeEach(() => { + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mgw-validate-g3-')); + restoreCwd = overrideCwd(tmpDir); + }); + + afterEach(() => { + restoreCwd(); + cleanMgw(tmpDir); + delete _require.cache[STATE_MODULE]; + }); + + afterAll(() => { + if (tmpDir && fs.existsSync(tmpDir)) { + fs.rmSync(tmpDir, { recursive: true, force: true }); + } + }); + + it('T3.1 – returns null when .mgw/active/ is absent', () => { + const { loadActiveIssue } = loadState(); + expect(loadActiveIssue(123)).toBeNull(); + }); + + it('T3.2 – returns null when no matching file exists', () => { + const activeDir = path.join(tmpDir, '.mgw', 'active'); + fs.mkdirSync(activeDir, { recursive: true }); + fs.writeFileSync(path.join(activeDir, '99-other.json'), '{"issue_number":99}', 'utf-8'); + + const { loadActiveIssue } = loadState(); + expect(loadActiveIssue(123)).toBeNull(); + }); + + it('T3.3 – finds and returns file by numeric prefix pattern', () => { + const activeDir = path.join(tmpDir, '.mgw', 'active'); + fs.mkdirSync(activeDir, { recursive: true }); + const data = { issue_number: 123, title: 'My issue', pipeline_stage: 'triaged' }; + fs.writeFileSync(path.join(activeDir, '123-some-slug.json'), JSON.stringify(data), 'utf-8'); + + const { loadActiveIssue } = loadState(); + const loaded = loadActiveIssue(123); + expect(loaded).not.toBeNull(); + expect(loaded.issue_number).toBe(123); + expect(loaded.pipeline_stage).toBe('triaged'); + }); + + it('T3.4 – accepts string issue number', () => { + const activeDir = path.join(tmpDir, '.mgw', 'active'); + fs.mkdirSync(activeDir, { recursive: true }); + const data = { issue_number: 456, title: 'String test' }; + fs.writeFileSync(path.join(activeDir, '456-string-test.json'), JSON.stringify(data), 'utf-8'); + + const { loadActiveIssue } = loadState(); + const loaded = loadActiveIssue('456'); + expect(loaded).not.toBeNull(); + expect(loaded.issue_number).toBe(456); + }); +}); + +// --------------------------------------------------------------------------- +// Group 4: Staleness detection with mocked GitHub updatedAt timestamps +// --------------------------------------------------------------------------- + +describe('Group 4: Staleness detection with mocked GitHub timestamps', () => { + let tmpDir; + let restoreCwd; + let mockGitHub; + + beforeEach(() => { + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mgw-validate-g4-')); + restoreCwd = overrideCwd(tmpDir); + mockGitHub = tryLoadMockGitHub(); + // Deactivate any auto-activation from test/setup.js so we control it here + if (mockGitHub && typeof mockGitHub.deactivate === 'function') { + mockGitHub.deactivate(); + } + }); + + afterEach(() => { + if (mockGitHub && typeof mockGitHub.deactivate === 'function') { + mockGitHub.deactivate(); + } + restoreCwd(); + cleanMgw(tmpDir); + delete _require.cache[STATE_MODULE]; + }); + + afterAll(() => { + if (tmpDir && fs.existsSync(tmpDir)) { + fs.rmSync(tmpDir, { recursive: true, force: true }); + } + }); + + it('T4.1 – mock-github intercepts gh issue view and returns mocked updatedAt', () => { + if (!mockGitHub) { + // Gracefully skip when mock-github.cjs is not available + console.warn('T4.1 skipped: lib/mock-github.cjs not available (PR #247 not merged)'); + return; + } + + mockGitHub.activate(); + + const mockResponse = JSON.stringify({ number: 99, updatedAt: '2025-01-15T12:00:00Z' }); + mockGitHub.setResponse('gh issue view', mockResponse); + + // Use _require('child_process').execSync so we pick up the patched version + const childProcess = _require('child_process'); + let result; + try { + result = childProcess.execSync('gh issue view 99 --json updatedAt', { encoding: 'utf-8' }); + } catch (err) { + result = err.stdout || ''; + } + + const parsed = JSON.parse(result.trim()); + expect(parsed.updatedAt).toBe('2025-01-15T12:00:00Z'); + + mockGitHub.deactivate(); + }); + + it('T4.2 – stale detection via comment count mismatch (stored=2, current=5)', () => { + if (!mockGitHub) { + console.warn('T4.2 skipped: lib/mock-github.cjs not available'); + return; + } + + // Write active issue with stored comment count of 2 + const activeDir = path.join(tmpDir, '.mgw', 'active'); + fs.mkdirSync(activeDir, { recursive: true }); + const issueState = { + issue_number: 99, + triage: { last_comment_count: 2 }, + }; + fs.writeFileSync(path.join(activeDir, '99-test-issue.json'), JSON.stringify(issueState), 'utf-8'); + + // Mock GitHub to return 5 comments + mockGitHub.activate(); + const mockComments = JSON.stringify({ + comments: Array.from({ length: 5 }, (_, i) => ({ + author: { login: 'user' }, + body: `Comment ${i + 1}`, + createdAt: '2025-01-01T00:00:00Z', + })), + }); + mockGitHub.setResponse('gh issue view', mockComments); + + const childProcess = _require('child_process'); + let commentData; + try { + commentData = childProcess.execSync('gh issue view 99 --json comments', { encoding: 'utf-8' }); + } catch (err) { + commentData = err.stdout || '{"comments":[]}'; + } + + const parsed = JSON.parse(commentData.trim()); + const currentCount = Array.isArray(parsed.comments) ? parsed.comments.length : 0; + const storedCount = issueState.triage.last_comment_count; + + // Assert staleness detected: current > stored + expect(currentCount).toBeGreaterThan(storedCount); + expect(currentCount - storedCount).toBe(3); + + mockGitHub.deactivate(); + }); + + it('T4.3 – no staleness when comment counts match', () => { + if (!mockGitHub) { + console.warn('T4.3 skipped: lib/mock-github.cjs not available'); + return; + } + + const activeDir = path.join(tmpDir, '.mgw', 'active'); + fs.mkdirSync(activeDir, { recursive: true }); + const issueState = { + issue_number: 99, + triage: { last_comment_count: 2 }, + }; + fs.writeFileSync(path.join(activeDir, '99-test-no-stale.json'), JSON.stringify(issueState), 'utf-8'); + + // Mock returns exactly 2 comments (same as stored) + mockGitHub.activate(); + const mockComments = JSON.stringify({ + comments: Array.from({ length: 2 }, (_, i) => ({ + author: { login: 'user' }, + body: `Comment ${i + 1}`, + createdAt: '2025-01-01T00:00:00Z', + })), + }); + mockGitHub.setResponse('gh issue view', mockComments); + + const childProcess = _require('child_process'); + let commentData; + try { + commentData = childProcess.execSync('gh issue view 99 --json comments', { encoding: 'utf-8' }); + } catch (err) { + commentData = err.stdout || '{"comments":[]}'; + } + + const parsed = JSON.parse(commentData.trim()); + const currentCount = Array.isArray(parsed.comments) ? parsed.comments.length : 0; + const storedCount = issueState.triage.last_comment_count; + + // No staleness: counts are equal + expect(currentCount).toBe(storedCount); + expect(currentCount > storedCount).toBe(false); + + mockGitHub.deactivate(); + }); + + it('T4.4 – mock-github call log captures gh commands', () => { + if (!mockGitHub) { + console.warn('T4.4 skipped: lib/mock-github.cjs not available'); + return; + } + + mockGitHub.activate(); + mockGitHub.clearCallLog(); + + mockGitHub.setResponse('gh issue view', JSON.stringify({ number: 77 })); + + const childProcess = _require('child_process'); + try { + childProcess.execSync('gh issue view 77 --json number', { encoding: 'utf-8' }); + } catch { + // ignore exit code + } + + const log = mockGitHub.getCallLog(); + expect(Array.isArray(log)).toBe(true); + expect(log.length).toBeGreaterThanOrEqual(1); + + const entry = log[0]; + expect(entry).toHaveProperty('cmd'); + expect(entry.cmd).toContain('gh'); + + mockGitHub.deactivate(); + }); +}); + +// --------------------------------------------------------------------------- +// Group 5: loadCrossRefs validation +// --------------------------------------------------------------------------- + +describe('Group 5: loadCrossRefs validation', () => { + let tmpDir; + let restoreCwd; + + beforeEach(() => { + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mgw-validate-g5-')); + restoreCwd = overrideCwd(tmpDir); + }); + + afterEach(() => { + restoreCwd(); + cleanMgw(tmpDir); + delete _require.cache[STATE_MODULE]; + }); + + afterAll(() => { + if (tmpDir && fs.existsSync(tmpDir)) { + fs.rmSync(tmpDir, { recursive: true, force: true }); + } + }); + + it('T5.1 – returns empty links when cross-refs.json is absent', () => { + const { loadCrossRefs } = loadState(); + const result = loadCrossRefs(); + expect(result.links).toEqual([]); + expect(result.warnings).toEqual([]); + }); + + it('T5.2 – parses valid cross-refs.json and returns links', () => { + const mgwDir = path.join(tmpDir, '.mgw'); + fs.mkdirSync(mgwDir, { recursive: true }); + + const data = { + links: [ + { a: 'issue:1', b: 'issue:2', type: 'related' }, + ], + }; + fs.writeFileSync(path.join(mgwDir, 'cross-refs.json'), JSON.stringify(data), 'utf-8'); + + const { loadCrossRefs } = loadState(); + const result = loadCrossRefs(); + expect(result.links).toHaveLength(1); + expect(result.links[0]).toMatchObject({ a: 'issue:1', b: 'issue:2', type: 'related' }); + expect(result.warnings).toEqual([]); + }); + + it('T5.3 – skips invalid links and adds warnings for missing "a"', () => { + const mgwDir = path.join(tmpDir, '.mgw'); + fs.mkdirSync(mgwDir, { recursive: true }); + + const data = { + links: [ + { b: 'issue:2', type: 'related' }, // missing "a" + ], + }; + fs.writeFileSync(path.join(mgwDir, 'cross-refs.json'), JSON.stringify(data), 'utf-8'); + + const { loadCrossRefs } = loadState(); + const result = loadCrossRefs(); + expect(result.links).toHaveLength(0); + expect(result.warnings.length).toBeGreaterThan(0); + expect(result.warnings[0]).toContain('missing "a"'); + }); + + it('T5.4 – returns warning when cross-refs.json has malformed JSON', () => { + const mgwDir = path.join(tmpDir, '.mgw'); + fs.mkdirSync(mgwDir, { recursive: true }); + fs.writeFileSync(path.join(mgwDir, 'cross-refs.json'), 'NOT JSON', 'utf-8'); + + const { loadCrossRefs } = loadState(); + const result = loadCrossRefs(); + expect(result.links).toEqual([]); + expect(result.warnings.length).toBeGreaterThan(0); + expect(result.warnings[0]).toContain('parse error'); + }); + + it('T5.5 – returns warning when cross-refs.json has no links array', () => { + const mgwDir = path.join(tmpDir, '.mgw'); + fs.mkdirSync(mgwDir, { recursive: true }); + fs.writeFileSync(path.join(mgwDir, 'cross-refs.json'), JSON.stringify({ entries: [] }), 'utf-8'); + + const { loadCrossRefs } = loadState(); + const result = loadCrossRefs(); + expect(result.links).toEqual([]); + expect(result.warnings.length).toBeGreaterThan(0); + }); +});