From 07639e3dfa36f9eaf2bfa79108c7f5ba7873cb3f Mon Sep 17 00:00:00 2001 From: prakashaditya13 Date: Fri, 21 Nov 2025 06:27:39 +0530 Subject: [PATCH 1/7] Add: Test suits files for all managers --- .gitignore | 4 +- jsconfig.json | 8 + package-lock.json | 1976 ++++++++++++++++++++++++-- package.json | 16 +- src/cli/index.ts | 1 - src/managers/MergeManager.ts | 9 +- tests/helpers/testRepo.ts | 16 + tests/mocks/inquirerMock.ts | 51 + tests/unit/BranchManager.test.ts | 190 +++ tests/unit/CommitManager.test.ts | 227 +++ tests/unit/ConflictManager.test.ts | 189 +++ tests/unit/HistoryManager.test.ts | 227 +++ tests/unit/MergeManager.test.ts | 256 ++++ tests/unit/RebaseManager.test.ts | 275 ++++ tests/unit/RemoteManager.test.ts | 265 ++++ tests/unit/RepositoryManager.test.ts | 260 ++++ tests/unit/ResetManager.test.ts | 244 ++++ tests/unit/StashManager.test.ts | 195 +++ tests/unit/TagManager.test.ts | 175 +++ tsconfig.json | 12 +- vitest.config.ts | 20 + 21 files changed, 4465 insertions(+), 151 deletions(-) create mode 100644 jsconfig.json create mode 100644 tests/helpers/testRepo.ts create mode 100644 tests/mocks/inquirerMock.ts create mode 100644 tests/unit/BranchManager.test.ts create mode 100644 tests/unit/CommitManager.test.ts create mode 100644 tests/unit/ConflictManager.test.ts create mode 100644 tests/unit/HistoryManager.test.ts create mode 100644 tests/unit/MergeManager.test.ts create mode 100644 tests/unit/RebaseManager.test.ts create mode 100644 tests/unit/RemoteManager.test.ts create mode 100644 tests/unit/RepositoryManager.test.ts create mode 100644 tests/unit/ResetManager.test.ts create mode 100644 tests/unit/StashManager.test.ts create mode 100644 tests/unit/TagManager.test.ts create mode 100644 vitest.config.ts diff --git a/.gitignore b/.gitignore index 6f66877..a1dc866 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,6 @@ node_modules dist .DS_Store .env -coverage \ No newline at end of file +coverage +tests/sandbox/ +tests/sandbox/repo/ diff --git a/jsconfig.json b/jsconfig.json new file mode 100644 index 0000000..486c7be --- /dev/null +++ b/jsconfig.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "baseUrl": "./", + "paths": { + "@/*": ["src/*"] + } + } +} diff --git a/package-lock.json b/package-lock.json index 95fd67b..c9febaa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,17 +18,25 @@ }, "devDependencies": { "@types/jest": "^30.0.0", - "@types/node": "^24.10.0", + "@types/node": "^24.10.1", "eslint": "^9.39.1", + "execa": "^9.6.0", "husky": "^9.1.7", "jest": "^30.2.0", "lint-staged": "^16.2.6", "nodemon": "^3.1.10", "prettier": "^3.6.2", "rimraf": "^6.1.0", + "strip-ansi": "^7.1.2", "ts-jest": "^29.4.5", "ts-node": "^10.9.2", - "typescript": "^5.9.3" + "tsconfig-paths": "^4.2.0", + "tsx": "^4.20.6", + "typescript": "^5.9.3", + "vitest": "^4.0.9" + }, + "engines": { + "node": ">=16" } }, "node_modules/@babel/code-frame": { @@ -563,26 +571,468 @@ "tslib": "^2.4.0" } }, - "node_modules/@emnapi/runtime": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.7.0.tgz", - "integrity": "sha512-oAYoQnCYaQZKVS53Fq23ceWMRxq5EhQsE0x0RdQ55jT7wagMu5k+fS39v1fiSLrtrLQlXwVINenqhLMtTrV/1Q==", + "node_modules/@emnapi/runtime": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.7.0.tgz", + "integrity": "sha512-oAYoQnCYaQZKVS53Fq23ceWMRxq5EhQsE0x0RdQ55jT7wagMu5k+fS39v1fiSLrtrLQlXwVINenqhLMtTrV/1Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz", + "integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], "dev": true, "license": "MIT", "optional": true, - "dependencies": { - "tslib": "^2.4.0" + "os": [ + "win32" + ], + "engines": { + "node": ">=18" } }, - "node_modules/@emnapi/wasi-threads": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz", - "integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==", + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", "optional": true, - "dependencies": { - "tslib": "^2.4.0" + "os": [ + "win32" + ], + "engines": { + "node": ">=18" } }, "node_modules/@eslint-community/eslint-utils": { @@ -1814,114 +2264,429 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/gen-mapping/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", + "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", + "@tybys/wasm-util": "^0.10.0" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@pkgr/core": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz", + "integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/pkgr" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.2.tgz", + "integrity": "sha512-yDPzwsgiFO26RJA4nZo8I+xqzh7sJTZIWQOxn+/XOdPE31lAvLIYCKqjV+lNH/vxE2L2iH3plKxDCRK6i+CwhA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.2.tgz", + "integrity": "sha512-k8FontTxIE7b0/OGKeSN5B6j25EuppBcWM33Z19JoVT7UTXFSo3D9CdU39wGTeb29NO3XxpMNauh09B+Ibw+9g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.2.tgz", + "integrity": "sha512-A6s4gJpomNBtJ2yioj8bflM2oogDwzUiMl2yNJ2v9E7++sHrSrsQ29fOfn5DM/iCzpWcebNYEdXpaK4tr2RhfQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.2.tgz", + "integrity": "sha512-e6XqVmXlHrBlG56obu9gDRPW3O3hLxpwHpLsBJvuI8qqnsrtSZ9ERoWUXtPOkY8c78WghyPHZdmPhHLWNdAGEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.2.tgz", + "integrity": "sha512-v0E9lJW8VsrwPux5Qe5CwmH/CF/2mQs6xU1MF3nmUxmZUCHazCjLgYvToOk+YuuUqLQBio1qkkREhxhc656ViA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.2.tgz", + "integrity": "sha512-ClAmAPx3ZCHtp6ysl4XEhWU69GUB1D+s7G9YjHGhIGCSrsg00nEGRRZHmINYxkdoJehde8VIsDC5t9C0gb6yqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.2.tgz", + "integrity": "sha512-EPlb95nUsz6Dd9Qy13fI5kUPXNSljaG9FiJ4YUGU1O/Q77i5DYFW5KR8g1OzTcdZUqQQ1KdDqsTohdFVwCwjqg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.2.tgz", + "integrity": "sha512-BOmnVW+khAUX+YZvNfa0tGTEMVVEerOxN0pDk2E6N6DsEIa2Ctj48FOMfNDdrwinocKaC7YXUZ1pHlKpnkja/Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.2.tgz", + "integrity": "sha512-Xt2byDZ+6OVNuREgBXr4+CZDJtrVso5woFtpKdGPhpTPHcNG7D8YXeQzpNbFRxzTVqJf7kvPMCub/pcGUWgBjA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.2.tgz", + "integrity": "sha512-+LdZSldy/I9N8+klim/Y1HsKbJ3BbInHav5qE9Iy77dtHC/pibw1SR/fXlWyAk0ThnpRKoODwnAuSjqxFRDHUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.2.tgz", + "integrity": "sha512-8ms8sjmyc1jWJS6WdNSA23rEfdjWB30LH8Wqj0Cqvv7qSHnvw6kgMMXRdop6hkmGPlyYBdRPkjJnj3KCUHV/uQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.2.tgz", + "integrity": "sha512-3HRQLUQbpBDMmzoxPJYd3W6vrVHOo2cVW8RUo87Xz0JPJcBLBr5kZ1pGcQAhdZgX9VV7NbGNipah1omKKe23/g==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.2.tgz", + "integrity": "sha512-fMjKi+ojnmIvhk34gZP94vjogXNNUKMEYs+EDaB/5TG/wUkoeua7p7VCHnE6T2Tx+iaghAqQX8teQzcvrYpaQA==", + "cpu": [ + "riscv64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" - } + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@jridgewell/gen-mapping/node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.2.tgz", + "integrity": "sha512-XuGFGU+VwUUV5kLvoAdi0Wz5Xbh2SrjIxCtZj6Wq8MDp4bflb/+ThZsVxokM7n0pcbkEr2h5/pzqzDYI7cCgLQ==", + "cpu": [ + "riscv64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@jridgewell/remapping": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", - "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.2.tgz", + "integrity": "sha512-w6yjZF0P+NGzWR3AXWX9zc0DNEGdtvykB03uhonSHMRa+oWA6novflo2WaJr6JZakG2ucsyb+rvhrKac6NIy+w==", + "cpu": [ + "s390x" + ], "dev": true, "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - } + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@jridgewell/remapping/node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.2.tgz", + "integrity": "sha512-yo8d6tdfdeBArzC7T/PnHd7OypfI9cbuZzPnzLJIyKYFhAQ8SvlkKtKBMbXDxe1h03Rcr7u++nFS7tqXz87Gtw==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.2.tgz", + "integrity": "sha512-ah59c1YkCxKExPP8O9PwOvs+XRLKwh/mV+3YdKqQ5AMQ0r4M4ZDuOrpWkUaqO7fzAHdINzV9tEVu8vNw48z0lA==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "engines": { - "node": ">=6.0.0" - } + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.2.tgz", + "integrity": "sha512-4VEd19Wmhr+Zy7hbUsFZ6YXEiP48hE//KPLCSVNY5RMGX2/7HZ+QkN55a3atM1C/BZCGIgqN+xrVgtdak2S9+A==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", - "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.2.tgz", + "integrity": "sha512-IlbHFYc/pQCgew/d5fslcy1KEaYVCJ44G8pajugd8VoOEI8ODhtb/j8XMhLpwHCMB3yk2J07ctup10gpw2nyMA==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.0.3", - "@jridgewell/sourcemap-codec": "^1.4.10" - } + "optional": true, + "os": [ + "win32" + ] }, - "node_modules/@napi-rs/wasm-runtime": { - "version": "0.2.12", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", - "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==", + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.2.tgz", + "integrity": "sha512-lNlPEGgdUfSzdCWU176ku/dQRnA7W+Gp8d+cWv73jYrb8uT7HTVVxq62DUYxjbaByuf1Yk0RIIAbDzp+CnOTFg==", + "cpu": [ + "ia32" + ], "dev": true, "license": "MIT", "optional": true, - "dependencies": { - "@emnapi/core": "^1.4.3", - "@emnapi/runtime": "^1.4.3", - "@tybys/wasm-util": "^0.10.0" - } + "os": [ + "win32" + ] }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.2.tgz", + "integrity": "sha512-S6YojNVrHybQis2lYov1sd+uj7K0Q05NxHcGktuMMdIQ2VixGwAfbJ23NnlvvVV1bdpR2m5MsNBViHJKcA4ADw==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", "optional": true, - "engines": { - "node": ">=14" - } + "os": [ + "win32" + ] }, - "node_modules/@pkgr/core": { - "version": "0.2.9", - "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz", - "integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==", + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.2.tgz", + "integrity": "sha512-k+/Rkcyx//P6fetPoLMb8pBeqJBNGx81uuf7iljX9++yNBVRDQgD04L+SVXmXmh5ZP4/WOp4mWF0kmi06PW2tA==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/pkgr" - } + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@sec-ant/readable-stream": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", + "integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==", + "dev": true, + "license": "MIT" }, "node_modules/@sinclair/typebox": { "version": "0.34.41", @@ -1930,6 +2695,19 @@ "dev": true, "license": "MIT" }, + "node_modules/@sindresorhus/merge-streams": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz", + "integrity": "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@sinonjs/commons": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", @@ -1950,6 +2728,13 @@ "@sinonjs/commons": "^3.0.1" } }, + "node_modules/@standard-schema/spec": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", + "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", + "dev": true, + "license": "MIT" + }, "node_modules/@tsconfig/node10": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", @@ -2034,6 +2819,24 @@ "@babel/types": "^7.28.2" } }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -2087,9 +2890,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "24.10.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.0.tgz", - "integrity": "sha512-qzQZRBqkFsYyaSWXuEHc2WR9c0a0CXwiE5FWUvn7ZM+vdy1uZLfCunD38UzhuB7YN/J11ndbDBcTmOdxJo9Q7A==", + "version": "24.10.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz", + "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==", "devOptional": true, "license": "MIT", "dependencies": { @@ -2396,6 +3199,117 @@ "win32" ] }, + "node_modules/@vitest/expect": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.9.tgz", + "integrity": "sha512-C2vyXf5/Jfj1vl4DQYxjib3jzyuswMi/KHHVN2z+H4v16hdJ7jMZ0OGe3uOVIt6LyJsAofDdaJNIFEpQcrSTFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.0.9", + "@vitest/utils": "4.0.9", + "chai": "^6.2.0", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.9.tgz", + "integrity": "sha512-PUyaowQFHW+9FKb4dsvvBM4o025rWMlEDXdWRxIOilGaHREYTi5Q2Rt9VCgXgPy/hHZu1LeuXtrA/GdzOatP2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.0.9", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.9.tgz", + "integrity": "sha512-Hor0IBTwEi/uZqB7pvGepyElaM8J75pYjrrqbC8ZYMB9/4n5QA63KC15xhT+sqHpdGWfdnPo96E8lQUxs2YzSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.9.tgz", + "integrity": "sha512-aF77tsXdEvIJRkj9uJZnHtovsVIx22Ambft9HudC+XuG/on1NY/bf5dlDti1N35eJT+QZLb4RF/5dTIG18s98w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.0.9", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.9.tgz", + "integrity": "sha512-r1qR4oYstPbnOjg0Vgd3E8ADJbi4ditCzqr+Z9foUrRhIy778BleNyZMeAJ2EjV+r4ASAaDsdciC9ryMy8xMMg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.0.9", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.9.tgz", + "integrity": "sha512-J9Ttsq0hDXmxmT8CUOWUr1cqqAj2FJRGTdyEjSR+NjoOGKEqkEWj+09yC0HhI8t1W6t4Ctqawl1onHgipJve1A==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.9.tgz", + "integrity": "sha512-cEol6ygTzY4rUPvNZM19sDf7zGa35IYTm9wfzkHoT/f5jX10IOY7QleWSOh5T0e3I3WVozwK5Asom79qW8DiuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.0.9", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/acorn": { "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", @@ -2521,6 +3435,16 @@ "dev": true, "license": "Python-2.0" }, + "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/babel-jest": { "version": "30.2.0", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.2.0.tgz", @@ -2796,6 +3720,16 @@ ], "license": "CC-BY-4.0" }, + "node_modules/chai": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.1.tgz", + "integrity": "sha512-p4Z49OGG5W/WBCPSS/dH3jQ73kD6tiMmUM+bckNK6Jr5JHMG3k9bg/BvKR8lKmtVBKmOiuVaV2ws8s9oSbwysg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/chalk": { "version": "5.6.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", @@ -3247,6 +4181,55 @@ "is-arrayish": "^0.2.1" } }, + "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.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -3445,6 +4428,16 @@ "node": ">=4.0" } }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -3463,36 +4456,32 @@ "license": "MIT" }, "node_modules/execa": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", - "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-9.6.0.tgz", + "integrity": "sha512-jpWzZ1ZhwUmeWRhS7Qv3mhpOhLfwI+uAX4e5fOcXqwMR7EcJ0pj2kV1CVzHVMX/LphnKWD3LObjZCoJ71lKpHw==", "dev": true, "license": "MIT", "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^6.0.0", - "human-signals": "^2.1.0", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.1", - "onetime": "^5.1.2", - "signal-exit": "^3.0.3", - "strip-final-newline": "^2.0.0" + "@sindresorhus/merge-streams": "^4.0.0", + "cross-spawn": "^7.0.6", + "figures": "^6.1.0", + "get-stream": "^9.0.0", + "human-signals": "^8.0.1", + "is-plain-obj": "^4.1.0", + "is-stream": "^4.0.1", + "npm-run-path": "^6.0.0", + "pretty-ms": "^9.2.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^4.0.0", + "yoctocolors": "^2.1.1" }, "engines": { - "node": ">=10" + "node": "^18.19.0 || >=20.5.0" }, "funding": { "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, - "node_modules/execa/node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true, - "license": "ISC" - }, "node_modules/exit-x": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/exit-x/-/exit-x-0.2.2.tgz", @@ -3521,6 +4510,16 @@ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, + "node_modules/expect-type": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.2.tgz", + "integrity": "sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==", + "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", @@ -3567,6 +4566,22 @@ "node": ">= 17.0.0" } }, + "node_modules/figures": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz", + "integrity": "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-unicode-supported": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -3714,18 +4729,35 @@ } }, "node_modules/get-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz", + "integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==", "dev": true, "license": "MIT", + "dependencies": { + "@sec-ant/readable-stream": "^0.4.1", + "is-stream": "^4.0.1" + }, "engines": { - "node": ">=10" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/get-tsconfig": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz", + "integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, "node_modules/glob": { "version": "10.4.5", "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", @@ -3846,13 +4878,13 @@ "license": "MIT" }, "node_modules/human-signals": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", - "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-8.0.1.tgz", + "integrity": "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==", "dev": true, "license": "Apache-2.0", "engines": { - "node": ">=10.17.0" + "node": ">=18.18.0" } }, "node_modules/husky": { @@ -4075,14 +5107,40 @@ "node": ">=0.12.0" } }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz", + "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==", "dev": true, "license": "MIT", "engines": { - "node": ">=8" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -4248,6 +5306,96 @@ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, + "node_modules/jest-changed-files/node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/jest-changed-files/node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-changed-files/node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/jest-changed-files/node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-changed-files/node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-changed-files/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/jest-changed-files/node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/jest-circus": { "version": "30.2.0", "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-30.2.0.tgz", @@ -5396,6 +6544,16 @@ "yallist": "^3.0.2" } }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, "node_modules/make-dir": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", @@ -5548,6 +6706,25 @@ "url": "https://github.com/sindresorhus/nano-spawn?sponsor=1" } }, + "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/napi-postinstall": { "version": "0.3.4", "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz", @@ -5668,16 +6845,33 @@ } }, "node_modules/npm-run-path": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", - "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-6.0.0.tgz", + "integrity": "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==", "dev": true, "license": "MIT", "dependencies": { - "path-key": "^3.0.0" + "path-key": "^4.0.0", + "unicorn-magic": "^0.3.0" }, "engines": { - "node": ">=8" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/once": { @@ -5805,6 +6999,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/parse-ms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-4.0.0.tgz", + "integrity": "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -5859,6 +7066,13 @@ "dev": true, "license": "ISC" }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -5971,6 +7185,35 @@ "node": ">=8" } }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "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", @@ -6022,7 +7265,23 @@ "node": ">=10" }, "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/pretty-ms": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.3.0.tgz", + "integrity": "sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parse-ms": "^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/pstree.remy": { @@ -6122,6 +7381,16 @@ "node": ">=4" } }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, "node_modules/restore-cursor": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", @@ -6265,6 +7534,48 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/rollup": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.2.tgz", + "integrity": "sha512-MHngMYwGJVi6Fmnk6ISmnk7JAHRNF0UkuucA0CUW3N3a4KnONPEZz+vUanQP/ZC/iY1Qkf3bwPWzyY84wEks1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.53.2", + "@rollup/rollup-android-arm64": "4.53.2", + "@rollup/rollup-darwin-arm64": "4.53.2", + "@rollup/rollup-darwin-x64": "4.53.2", + "@rollup/rollup-freebsd-arm64": "4.53.2", + "@rollup/rollup-freebsd-x64": "4.53.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.53.2", + "@rollup/rollup-linux-arm-musleabihf": "4.53.2", + "@rollup/rollup-linux-arm64-gnu": "4.53.2", + "@rollup/rollup-linux-arm64-musl": "4.53.2", + "@rollup/rollup-linux-loong64-gnu": "4.53.2", + "@rollup/rollup-linux-ppc64-gnu": "4.53.2", + "@rollup/rollup-linux-riscv64-gnu": "4.53.2", + "@rollup/rollup-linux-riscv64-musl": "4.53.2", + "@rollup/rollup-linux-s390x-gnu": "4.53.2", + "@rollup/rollup-linux-x64-gnu": "4.53.2", + "@rollup/rollup-linux-x64-musl": "4.53.2", + "@rollup/rollup-openharmony-arm64": "4.53.2", + "@rollup/rollup-win32-arm64-msvc": "4.53.2", + "@rollup/rollup-win32-ia32-msvc": "4.53.2", + "@rollup/rollup-win32-x64-gnu": "4.53.2", + "@rollup/rollup-win32-x64-msvc": "4.53.2", + "fsevents": "~2.3.2" + } + }, "node_modules/run-async": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/run-async/-/run-async-4.0.6.tgz", @@ -6322,6 +7633,13 @@ "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/signal-exit": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", @@ -6410,6 +7728,16 @@ "node": ">=0.10.0" } }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map-support": { "version": "0.5.13", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", @@ -6451,6 +7779,20 @@ "node": ">=8" } }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "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/string-argv": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", @@ -6623,13 +7965,16 @@ } }, "node_modules/strip-final-newline": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", - "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-4.0.0.tgz", + "integrity": "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==", "dev": true, "license": "MIT", "engines": { - "node": ">=6" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/strip-json-comments": { @@ -6711,6 +8056,78 @@ "url": "https://github.com/sponsors/isaacs" } }, + "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/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/tinyrainbow": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz", + "integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -6864,12 +8281,57 @@ } } }, + "node_modules/tsconfig-paths": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", + "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "json5": "^2.2.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tsconfig-paths/node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, + "node_modules/tsx": { + "version": "4.20.6", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.6.tgz", + "integrity": "sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.25.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -6948,6 +8410,19 @@ "devOptional": true, "license": "MIT" }, + "node_modules/unicorn-magic": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz", + "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/unrs-resolver": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.11.1.tgz", @@ -7057,6 +8532,203 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/vite": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.2.2.tgz", + "integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/vitest": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.9.tgz", + "integrity": "sha512-E0Ja2AX4th+CG33yAFRC+d1wFx2pzU5r6HtG6LiPSE04flaE0qB6YyjSw9ZcpJAtVPfsvZGtJlKWZpuW7EHRxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.0.9", + "@vitest/mocker": "4.0.9", + "@vitest/pretty-format": "4.0.9", + "@vitest/runner": "4.0.9", + "@vitest/snapshot": "4.0.9", + "@vitest/spy": "4.0.9", + "@vitest/utils": "4.0.9", + "debug": "^4.4.3", + "es-module-lexer": "^1.7.0", + "expect-type": "^1.2.2", + "magic-string": "^0.30.21", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^3.10.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.0.3", + "vite": "^6.0.0 || ^7.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.0.9", + "@vitest/browser-preview": "4.0.9", + "@vitest/browser-webdriverio": "4.0.9", + "@vitest/ui": "4.0.9", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/walker": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", @@ -7083,6 +8755,23 @@ "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": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -7363,6 +9052,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/yoctocolors": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.2.tgz", + "integrity": "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/yoctocolors-cjs": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.3.tgz", diff --git a/package.json b/package.json index 8203554..790a587 100644 --- a/package.json +++ b/package.json @@ -6,10 +6,11 @@ "type": "commonjs", "scripts": { "start": "ts-node src/cli/index.ts", - "dev": "nodemon --watch src --exec ts-node src/cli/index.ts", + "dev": "tsx -r tsconfig-paths/register src/cli/index.ts", "build": "tsc", - "test": "jest", - "clean": "rimraf dist" + "clean": "rimraf dist", + "cli": "tsx src/cli/index.ts", + "test": "vitest" }, "bin": { "plain-git": "./dist/cli/index.js" @@ -32,17 +33,22 @@ "license": "MIT", "devDependencies": { "@types/jest": "^30.0.0", - "@types/node": "^24.10.0", + "@types/node": "^24.10.1", "eslint": "^9.39.1", + "execa": "^9.6.0", "husky": "^9.1.7", "jest": "^30.2.0", "lint-staged": "^16.2.6", "nodemon": "^3.1.10", "prettier": "^3.6.2", "rimraf": "^6.1.0", + "strip-ansi": "^7.1.2", "ts-jest": "^29.4.5", "ts-node": "^10.9.2", - "typescript": "^5.9.3" + "tsconfig-paths": "^4.2.0", + "tsx": "^4.20.6", + "typescript": "^5.9.3", + "vitest": "^4.0.9" }, "dependencies": { "chalk": "^5.6.2", diff --git a/src/cli/index.ts b/src/cli/index.ts index 29b65e6..77e5bfe 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -7,7 +7,6 @@ import { Logger } from '../utils/Logger'; import { COMMANDS_LIST } from '../config/commandsList'; import { handleCommand } from '../core/HandleCommands'; import { BranchDetector } from '../core/BranchDetector'; -import { stdout } from 'process'; /** * plain-git CLI diff --git a/src/managers/MergeManager.ts b/src/managers/MergeManager.ts index 64f7b1a..e41b579 100644 --- a/src/managers/MergeManager.ts +++ b/src/managers/MergeManager.ts @@ -27,10 +27,10 @@ function isMergeInProgress(): boolean { */ function getConflictedFiles(): string[] { try { - const output = execSync('git diff --name-only --diff-filter=U', { + const output = (execSync('git diff --name-only --diff-filter=U', { encoding: 'utf-8', - }); - return output.split('\n').filter(Boolean); + }) as unknown as string) ?? ''; + return String(output).split('\n').filter(Boolean); } catch { return []; } @@ -50,7 +50,8 @@ export const MergeManager = { Logger.info('🔍 Fetching branch list...'); - const branchesOutput = execSync('git branch --all', { encoding: 'utf-8' }) + const branchListRaw = (execSync('git branch --all', { encoding: 'utf-8' }) as unknown as string) ?? ''; + const branchesOutput = String(branchListRaw) .split('\n') .map((b) => b.replace('*', '').trim()) .filter(Boolean) diff --git a/tests/helpers/testRepo.ts b/tests/helpers/testRepo.ts new file mode 100644 index 0000000..8a6c65a --- /dev/null +++ b/tests/helpers/testRepo.ts @@ -0,0 +1,16 @@ +import { execSync } from 'child_process'; +import fs from 'fs'; +import path from 'path'; + +export function createTestRepo() { + const repoPath = path.join(process.cwd(), 'tests', 'sandbox', 'repo'); + + if (fs.existsSync(repoPath)) { + fs.rmSync(repoPath, { recursive: true, force: true }); + } + + fs.mkdirSync(repoPath, { recursive: true }); + + execSync('git init', { cwd: repoPath }); + return repoPath; +} diff --git a/tests/mocks/inquirerMock.ts b/tests/mocks/inquirerMock.ts new file mode 100644 index 0000000..ccfdedb --- /dev/null +++ b/tests/mocks/inquirerMock.ts @@ -0,0 +1,51 @@ +import { vi } from "vitest"; +import inquirer from 'inquirer'; + +// Queue to hold successive mock answers. Each entry is an object +// mapping prompt `name` -> value (usually single-key objects used by tests). +const _answersQueue: Record[] = []; + +function ensurePromptMock() { + const curr = (inquirer as any).prompt; + if (curr && typeof curr.mockImplementation === 'function') return curr; + + // Replace prompt with a persistent mock implementation that consumes + // queued answers. It supports being called with a single question + // or an array of questions. + const mockFn = vi.fn().mockImplementation(async (questions: any) => { + // If questions is an array, we expect answers for each prompt to be queued + if (Array.isArray(questions)) { + const needed = questions.length; + // Merge next `needed` queued answer objects into one result + const toMerge: Record[] = []; + for (let i = 0; i < needed; i++) { + if (_answersQueue.length) { + toMerge.push(_answersQueue.shift() as Record); + } + } + return Object.assign({}, ...toMerge); + } + + // Single question: consume and return the next queued answer object + if (_answersQueue.length) { + return _answersQueue.shift(); + } + + return {}; + }); + + (inquirer as any).prompt = mockFn; + return mockFn; +} + +export function mockInquirer(answers: Record) { + // Enqueue the answers object for the next prompt(s) + _answersQueue.push(answers); + + try { + // Ensure the imported inquirer.prompt has been replaced with our persistent mock + ensurePromptMock(); + } catch (err) { + // ignore - tests will fail later if mock not present + } +} diff --git a/tests/unit/BranchManager.test.ts b/tests/unit/BranchManager.test.ts new file mode 100644 index 0000000..1c9ace0 --- /dev/null +++ b/tests/unit/BranchManager.test.ts @@ -0,0 +1,190 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { mockInquirer } from '../mocks/inquirerMock'; +import { GitExecutor } from '@/core/GitExecutor'; + +/** + * IMPORTANT: + * We must mock execSync BEFORE importing BranchManager + */ +vi.mock('child_process', async (importOriginal) => { + const actual: any = await importOriginal(); + return { + ...(actual || {}), + execSync: vi.fn(), + // ensure `exec` is available for modules that import it (GitExecutor) + exec: actual?.exec ?? vi.fn(), + } as any; +}); + +// Re-import execSync mock (will refer to the mocked function) +// @ts-ignore - mocked at runtime as a vitest mock +import { execSync } from 'child_process'; + +describe('BranchManager - Full Test Suite', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + // ------------------------------------------------------------- + // 1️⃣ createBranch() + // ------------------------------------------------------------- + it('should create a branch', async () => { + mockInquirer({ name: 'feature-x' }); + + const spy = vi.spyOn(GitExecutor, 'run').mockResolvedValue(); + + const { BranchManager } = await import('@/managers'); + + await BranchManager.createBranch(); + + expect(spy).toHaveBeenCalledWith('git branch feature-x'); + }); + + // ------------------------------------------------------------- + // 2️⃣ createAndSwitch() + // ------------------------------------------------------------- + it('should create and switch to a new branch', async () => { + mockInquirer({ name: 'dev-branch' }); + + const spy = vi.spyOn(GitExecutor, 'run').mockResolvedValue(); + + const { BranchManager } = await import('@/managers'); + + await BranchManager.createAndSwitch(); + + expect(spy).toHaveBeenCalledWith( + 'git switch -c dev-branch 2>/dev/null || git checkout -b dev-branch', + ); + }); + + // ------------------------------------------------------------- + // 3️⃣ switchBranch() + // ------------------------------------------------------------- + it('should switch to a branch', async () => { + (execSync as any).mockReturnValue('main\nfeature/login'); + + mockInquirer({ branch: 'feature/login' }); + + const spy = vi.spyOn(GitExecutor, 'run').mockResolvedValue(); + + const { BranchManager } = await import('@/managers'); + + await BranchManager.switchBranch(); + + expect(spy).toHaveBeenCalledWith( + 'git switch feature/login 2>/dev/null || git checkout feature/login', + ); + }); + + // ------------------------------------------------------------- + // 4️⃣ deleteBranch() + // ------------------------------------------------------------- + it('should safely delete a branch', async () => { + (execSync as any).mockReturnValue('main\nfix1'); + + mockInquirer({ branch: 'fix1' }); + mockInquirer({ force: false }); + + const spy = vi.spyOn(GitExecutor, 'run').mockResolvedValue(); + + const { BranchManager } = await import('@/managers'); + + await BranchManager.deleteBranch(); + + expect(spy).toHaveBeenCalledWith('git branch -d fix1'); + }); + + it('should force delete a branch', async () => { + (execSync as any).mockReturnValue('main\nfix2'); + + mockInquirer({ branch: 'fix2' }); + mockInquirer({ force: true }); + + const spy = vi.spyOn(GitExecutor, 'run').mockResolvedValue(); + + const { BranchManager } = await import('@/managers'); + + await BranchManager.deleteBranch(); + + expect(spy).toHaveBeenCalledWith('git branch -D fix2'); + }); + + // ------------------------------------------------------------- + // 5️⃣ renameBranch() + // ------------------------------------------------------------- + it('should rename a branch', async () => { + (execSync as any).mockReturnValue('main\nold-name'); + + mockInquirer({ branch: 'old-name' }); + mockInquirer({ newName: 'new-name' }); + + const spy = vi.spyOn(GitExecutor, 'run').mockResolvedValue(); + + const { BranchManager } = await import('@/managers'); + + await BranchManager.renameBranch(); + + expect(spy).toHaveBeenCalledWith('git branch -m old-name new-name'); + }); + + // ------------------------------------------------------------- + // 6️⃣ listBranches() + // ------------------------------------------------------------- + it('should list all branches', async () => { + const spy = vi.spyOn(GitExecutor, 'run').mockResolvedValue(); + + const { BranchManager } = await import('@/managers'); + + await BranchManager.listBranches(); + + expect(spy).toHaveBeenCalledWith('git branch --all --verbose --no-color'); + }); + + // ------------------------------------------------------------- + // 7️⃣ showCurrentBranch() + // ------------------------------------------------------------- + it('should show current branch', async () => { + (execSync as any).mockReturnValue('main'); + + const loggerInfo = vi.spyOn(console, 'log').mockImplementation(() => {}); + + const { BranchManager } = await import('@/managers'); + + await BranchManager.showCurrentBranch(); + + expect(loggerInfo).toHaveBeenCalled(); + }); + + // ------------------------------------------------------------- + // 8️⃣ pushBranch() + // ------------------------------------------------------------- + it('should push a branch to origin', async () => { + (execSync as any).mockReturnValue('main\nfeature/payments'); + + mockInquirer({ branch: 'feature/payments' }); + mockInquirer({ remote: 'origin' }); + + const spy = vi.spyOn(GitExecutor, 'run').mockResolvedValue(); + + const { BranchManager } = await import('@/managers'); + + await BranchManager.pushBranch(); + + expect(spy).toHaveBeenCalledWith('git push -u origin feature/payments'); + }); + + // ------------------------------------------------------------- + // 9️⃣ showAndList() + // ------------------------------------------------------------- + it('should call showCurrentBranch() then listBranches()', async () => { + (execSync as any).mockReturnValue('main'); + + const runSpy = vi.spyOn(GitExecutor, 'run').mockResolvedValue(); + + const { BranchManager } = await import('@/managers'); + + await BranchManager.showAndList(); + + expect(runSpy).toHaveBeenCalledWith('git branch --all --verbose --no-color'); + }); +}); diff --git a/tests/unit/CommitManager.test.ts b/tests/unit/CommitManager.test.ts new file mode 100644 index 0000000..31bfe90 --- /dev/null +++ b/tests/unit/CommitManager.test.ts @@ -0,0 +1,227 @@ +import { describe, it, beforeEach, expect, vi } from 'vitest'; +import { GitExecutor } from '@/core/GitExecutor'; +import { mockInquirer } from '../mocks/inquirerMock'; + +// Mock execSync BEFORE importing CommitManager +vi.mock('child_process', async (importOriginal) => { + const actual: any = await importOriginal(); + return { + ...(actual || {}), + execSync: vi.fn(), + // ensure `exec` is available for modules that import it (GitExecutor) + exec: actual?.exec ?? vi.fn(), + } as any; +}); + +import { execSync } from 'child_process'; + +describe('CommitManager - Full Test Suite', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + // ---------------------------------------------- + // stageAll() + // ---------------------------------------------- + it('should stage all files', async () => { + const spy = vi.spyOn(GitExecutor, 'run').mockResolvedValue(); + + const { CommitManager } = await import('@/managers'); + await CommitManager.stageAll(); + + expect(spy).toHaveBeenCalledWith('git add .'); + }); + + // ---------------------------------------------- + // stageFiles() + // ---------------------------------------------- + it('should not stage files when none exist', async () => { + (execSync as any).mockReturnValue(''); + + const { CommitManager } = await import('@/managers'); + + const spy = vi.spyOn(GitExecutor, 'run').mockResolvedValue(); + + await CommitManager.stageFiles(); + + expect(spy).not.toHaveBeenCalled(); + }); + + it('should not stage when user selects none', async () => { + (execSync as any).mockReturnValue(' M file1.js\n?? file2.js'); + + mockInquirer({ selected: [] }); + + const spy = vi.spyOn(GitExecutor, 'run').mockResolvedValue(); + + const { CommitManager } = await import('@/managers'); + + await CommitManager.stageFiles(); + + expect(spy).not.toHaveBeenCalled(); + }); + + it('should stage selected files', async () => { + (execSync as any).mockReturnValue(' M a.js\n M b.js'); + + mockInquirer({ selected: ['a.js', 'b.js'] }); + + const spy = vi.spyOn(GitExecutor, 'run').mockResolvedValue(); + + const { CommitManager } = await import('@/managers'); + + await CommitManager.stageFiles(); + + expect(spy).toHaveBeenCalledWith('git add a.js b.js'); + }); + + // ---------------------------------------------- + // unstageFiles() + // ---------------------------------------------- + it('should unstage selected files', async () => { + (execSync as any).mockReturnValue(' M a.js\n M b.js'); + + mockInquirer({ selected: ['a.js'] }); + + const spy = vi.spyOn(GitExecutor, 'run').mockResolvedValue(); + + const { CommitManager } = await import('@/managers'); + + await CommitManager.unstageFiles(); + + expect(spy).toHaveBeenCalledWith('git restore --staged a.js'); + }); + + // ---------------------------------------------- + // commitChanges() + // ---------------------------------------------- + it('should not commit when empty message', async () => { + mockInquirer({ message: ' ' }); + + const spy = vi.spyOn(GitExecutor, 'run').mockResolvedValue(); + + const { CommitManager } = await import('@/managers'); + + await CommitManager.commitChanges(); + + expect(spy).not.toHaveBeenCalled(); + }); + + it('should commit with message', async () => { + mockInquirer({ message: 'Initial commit' }); + + const spy = vi.spyOn(GitExecutor, 'run').mockResolvedValue(); + + const { CommitManager } = await import('@/managers'); + + await CommitManager.commitChanges(); + + expect(spy).toHaveBeenCalledWith(`git commit -m "Initial commit"`); + }); + + // ---------------------------------------------- + // amendLastCommit() + // ---------------------------------------------- + it('should amend last commit', async () => { + mockInquirer({ message: 'Fix typo' }); + + const spy = vi.spyOn(GitExecutor, 'run').mockResolvedValue(); + + const { CommitManager } = await import('@/managers'); + + await CommitManager.amendLastCommit(); + + expect(spy).toHaveBeenCalledWith(`git commit --amend -m "Fix typo"`); + }); + + // ---------------------------------------------- + // undoLastCommit() + // ---------------------------------------------- + it('should undo last commit (soft)', async () => { + const spy = vi.spyOn(GitExecutor, 'run').mockResolvedValue(); + + const { CommitManager } = await import('@/managers'); + + await CommitManager.undoLastCommit(); + + expect(spy).toHaveBeenCalledWith('git reset --soft HEAD~1'); + }); + + // ---------------------------------------------- + // showLastCommit() + // ---------------------------------------------- + it('should show last commit', async () => { + const spy = vi.spyOn(GitExecutor, 'run').mockResolvedValue(); + + const { CommitManager } = await import('@/managers'); + + await CommitManager.showLastCommit(); + + expect(spy).toHaveBeenCalledWith('git show HEAD --stat --pretty=medium'); + }); + + // ---------------------------------------------- + // showLog() + // ---------------------------------------------- + it('should show compact log', async () => { + mockInquirer({ format: '--oneline' }); + + const spy = vi.spyOn(GitExecutor, 'run').mockResolvedValue(); + + const { CommitManager } = await import('@/managers'); + + await CommitManager.showLog(); + + expect(spy).toHaveBeenCalledWith('git log --oneline'); + }); + + it('should show detailed log', async () => { + mockInquirer({ format: '' }); + + const spy = vi.spyOn(GitExecutor, 'run').mockResolvedValue(); + + const { CommitManager } = await import('@/managers'); + + await CommitManager.showLog(); + + expect(spy).toHaveBeenCalledWith('git log '); + }); + + it('should show graph log', async () => { + mockInquirer({ format: '--oneline --graph --decorate' }); + + const spy = vi.spyOn(GitExecutor, 'run').mockResolvedValue(); + + const { CommitManager } = await import('@/managers'); + + await CommitManager.showLog(); + + expect(spy).toHaveBeenCalledWith('git log --oneline --graph --decorate'); + }); + + // ---------------------------------------------- + // showDiff() + // ---------------------------------------------- + it('should show unstaged diff', async () => { + const spy = vi.spyOn(GitExecutor, 'run').mockResolvedValue(); + + const { CommitManager } = await import('@/managers'); + + await CommitManager.showDiff(); + + expect(spy).toHaveBeenCalledWith('git diff'); + }); + + // ---------------------------------------------- + // showStagedDiff() + // ---------------------------------------------- + it('should show staged diff', async () => { + const spy = vi.spyOn(GitExecutor, 'run').mockResolvedValue(); + + const { CommitManager } = await import('@/managers'); + + await CommitManager.showStagedDiff(); + + expect(spy).toHaveBeenCalledWith('git diff --cached'); + }); +}); diff --git a/tests/unit/ConflictManager.test.ts b/tests/unit/ConflictManager.test.ts new file mode 100644 index 0000000..98f892e --- /dev/null +++ b/tests/unit/ConflictManager.test.ts @@ -0,0 +1,189 @@ +import { describe, it, expect, beforeEach, vi } from "vitest"; +import { mockInquirer } from "@tests/mocks/inquirerMock"; +import { GitExecutor } from "@/core/GitExecutor"; + +// --------------------------- +// Correct fs mock +// --------------------------- +vi.mock("fs", async () => { + const actual = await vi.importActual("fs"); + return { + ...actual, + existsSync: vi.fn(), + readFileSync: vi.fn() + }; +}); + +// --------------------------- +// Correct execSync mock +// --------------------------- +vi.mock('child_process', async (importOriginal) => { + const actual: any = await importOriginal(); + return { + ...(actual || {}), + execSync: vi.fn(), + // ensure `exec` is available for modules that import it (GitExecutor) + exec: actual?.exec ?? vi.fn(), + // Mock spawn for GitExecutor.run + spawn: vi.fn(), + } as any; +}); + +import * as fs from "fs"; +import { execSync } from "child_process"; + +describe("ConflictManager – Full Test Suite", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + // --------------------------------------------------------- + // 1️⃣ listConflicts() + // --------------------------------------------------------- + it("should show no conflicts when none exist", async () => { + execSync.mockReturnValue(""); + + const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); + const { ConflictManager } = await import("@/managers"); + + await ConflictManager.listConflicts(); + + expect(spy).not.toHaveBeenCalled(); + }); + + it("should list conflicted files when they exist", async () => { + execSync.mockReturnValue("fileA.js\nfileB.ts\n"); + + const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); + const { ConflictManager } = await import("@/managers"); + + await ConflictManager.listConflicts(); + + expect(spy).not.toHaveBeenCalled(); // listConflicts only prints, no git run + }); + + // --------------------------------------------------------- + // 2️⃣ inspectConflict() + // --------------------------------------------------------- + it("should not inspect when no conflicts exist", async () => { + execSync.mockReturnValue(""); + + const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); + const { ConflictManager } = await import("@/managers"); + + await ConflictManager.inspectConflict(); + + expect(spy).not.toHaveBeenCalled(); + }); + + it("should inspect and show conflict markers", async () => { + execSync.mockReturnValue("conflict.txt"); + + fs.existsSync.mockReturnValue(true); + fs.readFileSync.mockReturnValue( + ` +line1 +<<<<<<< HEAD +your code +======= +their code +>>>>>>> branch +line9 +` + ); + + mockInquirer({ file: "conflict.txt" }); + + const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); + const { ConflictManager } = await import("@/managers"); + + await ConflictManager.inspectConflict(); + + expect(spy).not.toHaveBeenCalled(); // only prints markers + }); + + it("should show message when file has no conflict markers", async () => { + execSync.mockReturnValue("clean.txt"); + + fs.existsSync.mockReturnValue(true); + fs.readFileSync.mockReturnValue("normal content\nno conflict here"); + + mockInquirer({ file: "clean.txt" }); + + const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); + + const { ConflictManager } = await import("@/managers"); + + await ConflictManager.inspectConflict(); + + expect(spy).not.toHaveBeenCalled(); + }); + + // --------------------------------------------------------- + // 3️⃣ openInEditor() + // --------------------------------------------------------- + it("should not open editor if no conflicts", async () => { + execSync.mockReturnValue(""); + + const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); + const { ConflictManager } = await import("@/managers"); + + await ConflictManager.openInEditor(); + + expect(spy).not.toHaveBeenCalled(); + }); + + it("should open file in VSCode", async () => { + execSync.mockReturnValue("src/app.js"); + + mockInquirer({ file: "src/app.js" }); + + const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); + const { ConflictManager } = await import("@/managers"); + + await ConflictManager.openInEditor(); + + expect(spy).toHaveBeenCalledWith("code src/app.js"); + }); + + // --------------------------------------------------------- + // 4️⃣ showConflictDiff() + // --------------------------------------------------------- + it("should not diff when no conflicts exist", async () => { + execSync.mockReturnValue(""); + + const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); + + const { ConflictManager } = await import("@/managers"); + + await ConflictManager.showConflictDiff(); + + expect(spy).not.toHaveBeenCalled(); + }); + + it("should show diff for selected file", async () => { + execSync.mockReturnValue("conflict.txt"); + + mockInquirer({ file: "conflict.txt" }); + + const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); + const { ConflictManager } = await import("@/managers"); + + await ConflictManager.showConflictDiff(); + + expect(spy).toHaveBeenCalledWith("git diff conflict.txt"); + }); + + // --------------------------------------------------------- + // 5️⃣ conflictHelp() + // --------------------------------------------------------- + it("should show help text without errors", async () => { + const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {}); + + const { ConflictManager } = await import("@/managers"); + + await ConflictManager.conflictHelp(); + + expect(consoleSpy).toHaveBeenCalled(); // printed help text + }); +}); diff --git a/tests/unit/HistoryManager.test.ts b/tests/unit/HistoryManager.test.ts new file mode 100644 index 0000000..6f2963c --- /dev/null +++ b/tests/unit/HistoryManager.test.ts @@ -0,0 +1,227 @@ +import { describe, it, expect, beforeEach, vi } from "vitest"; +import { mockInquirer } from "../mocks/inquirerMock"; +import { GitExecutor } from "@/core/GitExecutor"; + +// Mock execSync BEFORE importing the manager +vi.mock('child_process', async (importOriginal) => { + const actual: any = await importOriginal(); + return { + ...(actual || {}), + execSync: vi.fn(), + // ensure `exec` is available for modules that import it (GitExecutor) + exec: actual?.exec ?? vi.fn(), + // Mock spawn for GitExecutor.run + spawn: vi.fn(), + } as any; +}); + +import { execSync } from "child_process"; + +describe("HistoryManager – Full Test Suite", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + // -------------------------------------------------------------- + // showHistoryGraph() + // -------------------------------------------------------------- + it("should run git log with chosen format (compact)", async () => { + mockInquirer({ format: "--oneline" }); + + const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); + + const { HistoryManager } = await import("@/managers"); + await HistoryManager.showHistoryGraph(); + + expect(spy).toHaveBeenCalledWith("git log --oneline"); + }); + + it("should run git log with chosen format (author/date)", async () => { + mockInquirer({ format: "--pretty=format:'%h - %an, %ar : %s'" }); + + const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); + + const { HistoryManager } = await import("@/managers"); + await HistoryManager.showHistoryGraph(); + + expect(spy).toHaveBeenCalledWith("git log --pretty=format:'%h - %an, %ar : %s'"); + }); + + // -------------------------------------------------------------- + // showReflog() + // -------------------------------------------------------------- + it("should run git reflog", async () => { + const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); + + const { HistoryManager } = await import("@/managers"); + await HistoryManager.showReflog(); + + expect(spy).toHaveBeenCalledWith("git reflog --date=relative"); + }); + + // -------------------------------------------------------------- + // showCommitDetails() + // -------------------------------------------------------------- + it("should not show commit details when no commits", async () => { + execSync.mockReturnValue(""); // getRecentCommits returns empty + + const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); + const { HistoryManager } = await import("@/managers"); + + await HistoryManager.showCommitDetails(); + + expect(spy).not.toHaveBeenCalled(); + }); + + it("should show commit details for selected commit", async () => { + // Simulate git log --oneline output + execSync.mockReturnValue("abc123 First commit\nbcd234 Second commit\n"); + + mockInquirer({ commit: "bcd234 Second commit" }); + + const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); + const { HistoryManager } = await import("@/managers"); + + await HistoryManager.showCommitDetails(); + + expect(spy).toHaveBeenCalledWith("git show bcd234 --stat"); + }); + + // -------------------------------------------------------------- + // compareCommits() + // -------------------------------------------------------------- + it("should not compare when less than two commits", async () => { + execSync.mockReturnValue("onlyonecommit\n"); + + const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); + const { HistoryManager } = await import("@/managers"); + + await HistoryManager.compareCommits(); + + expect(spy).not.toHaveBeenCalled(); + }); + + it("should compare two selected commits", async () => { + // Provide at least two commits + execSync.mockReturnValue( + "a1aaaaa Commit A\nb2bbbbb Commit B\nc3ccccc Commit C\n" + ); + + mockInquirer({ commit1: "a1aaaaa Commit A" }); + mockInquirer({ commit2: "c3ccccc Commit C" }); + + const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); + const { HistoryManager } = await import("@/managers"); + + await HistoryManager.compareCommits(); + + expect(spy).toHaveBeenCalledWith("git diff a1aaaaa c3ccccc"); + }); + + // -------------------------------------------------------------- + // showDiff() + // -------------------------------------------------------------- + it("should run git diff for working directory", async () => { + const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); + const { HistoryManager } = await import("@/managers"); + + await HistoryManager.showDiff(); + + expect(spy).toHaveBeenCalledWith("git diff"); + }); + + // -------------------------------------------------------------- + // showFileDiff() + // -------------------------------------------------------------- + it("should not show file diff when no tracked files", async () => { + execSync.mockReturnValue(""); // getTrackedFiles returns empty + + const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); + const { HistoryManager } = await import("@/managers"); + + await HistoryManager.showFileDiff(); + + expect(spy).not.toHaveBeenCalled(); + }); + + it("should show file diff for selected file", async () => { + execSync.mockReturnValue("src/index.js\nlib/util.js\n"); + + mockInquirer({ file: "lib/util.js" }); + + const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); + const { HistoryManager } = await import("@/managers"); + + await HistoryManager.showFileDiff(); + + expect(spy).toHaveBeenCalledWith("git diff lib/util.js"); + }); + + // -------------------------------------------------------------- + // showFileHistory() + // -------------------------------------------------------------- + it("should not show file history when no tracked files", async () => { + execSync.mockReturnValue(""); + + const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); + const { HistoryManager } = await import("@/managers"); + + await HistoryManager.showFileHistory(); + + expect(spy).not.toHaveBeenCalled(); + }); + + it("should show file history for selected file", async () => { + execSync.mockReturnValue("README.md\nsrc/app.js\n"); + + mockInquirer({ file: "README.md" }); + + const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); + const { HistoryManager } = await import("@/managers"); + + await HistoryManager.showFileHistory(); + + expect(spy).toHaveBeenCalledWith( + "git log --oneline --graph --decorate -- README.md" + ); + }); + + // -------------------------------------------------------------- + // blameFile() + // -------------------------------------------------------------- + it("should not run blame when no tracked files", async () => { + execSync.mockReturnValue(""); + + const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); + const { HistoryManager } = await import("@/managers"); + + await HistoryManager.blameFile(); + + expect(spy).not.toHaveBeenCalled(); + }); + + it("should run git blame for selected file", async () => { + execSync.mockReturnValue("index.js\nhelper.js\n"); + + mockInquirer({ file: "helper.js" }); + + const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); + const { HistoryManager } = await import("@/managers"); + + await HistoryManager.blameFile(); + + expect(spy).toHaveBeenCalledWith("git blame helper.js"); + }); + + // -------------------------------------------------------------- + // showAuthorSummary() + // -------------------------------------------------------------- + it("should run shortlog to generate author summary", async () => { + const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); + const { HistoryManager } = await import("@/managers"); + + await HistoryManager.showAuthorSummary(); + + expect(spy).toHaveBeenCalledWith("git shortlog -sn --all"); + }); +}); diff --git a/tests/unit/MergeManager.test.ts b/tests/unit/MergeManager.test.ts new file mode 100644 index 0000000..6e03efe --- /dev/null +++ b/tests/unit/MergeManager.test.ts @@ -0,0 +1,256 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { mockInquirer } from "../mocks/inquirerMock"; +import { GitExecutor } from "@/core/GitExecutor"; +import * as fs from "fs"; + +// MOCK FS + EXEC BEFORE IMPORT +vi.mock('child_process', async (importOriginal) => { + const actual: any = await importOriginal(); + return { + ...(actual || {}), + execSync: vi.fn(), + // ensure `exec` is available for modules that import it (GitExecutor) + exec: actual?.exec ?? vi.fn(), + // Mock spawn for GitExecutor.run + spawn: vi.fn(), + } as any; +}); + +// Mock `fs` so we can stub `existsSync` in tests +// Node's fs is a CommonJS module, so we need to mock both default and named exports +vi.mock("fs", async () => { + const actual = await vi.importActual("fs"); + const mockExistsSync = vi.fn(() => false); + return { + ...actual, + default: { + ...actual, + existsSync: mockExistsSync, + }, + existsSync: mockExistsSync, + }; +}); + +import { execSync } from "child_process"; + +describe("MergeManager – Full Test Suite", () => { + beforeEach(() => { + vi.clearAllMocks(); + // Reset fs.existsSync mock to default (false) + (fs.existsSync as any).mockReturnValue(false); + }); + + // -------------------------------------------------------------- + // 1️⃣ mergeBranch() + // -------------------------------------------------------------- + it("should block merge if a merge is already in progress", async () => { + (fs.existsSync as any).mockImplementation((path: string) => { + return path.includes("MERGE_HEAD"); + }); + + const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); + + const { MergeManager } = await import("@/managers"); + + await MergeManager.mergeBranch(); + + expect(spy).not.toHaveBeenCalled(); + }); + + it("should do nothing if no branches available", async () => { + (fs.existsSync as any).mockImplementation((path: string) => { + return false; // no merge active + }); + (execSync as any).mockReturnValue(""); // no branches at all + + const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); + const { MergeManager } = await import("@/managers"); + + await MergeManager.mergeBranch(); + + expect(spy).not.toHaveBeenCalled(); + }); + + it("should merge target branch successfully WITHOUT conflicts", async () => { + (fs.existsSync as any).mockImplementation((path: string) => { + return false; // no merge in progress + }); + + (execSync as any).mockImplementation((cmd: string) => { + if (cmd.includes("git branch --all")) { + return "* main\n feature\n dev\n"; + } + if (cmd.includes("git diff --name-only")) { + return ""; // no conflicts + } + return ""; + }); + + mockInquirer({ target: "feature" }); + + const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); + + const { MergeManager } = await import("@/managers"); + + await MergeManager.mergeBranch(); + + expect(spy).toHaveBeenCalledWith("git merge feature"); + }); + + it("should stop after printing conflicts when conflicts exist", async () => { + (fs.existsSync as any).mockImplementation((path: string) => { + return false; // no merge in progress + }); + + (execSync as any).mockImplementation((cmd: string) => { + if (cmd.includes("git branch --all")) { + return "main\nfeature"; + } + if (cmd.includes("git diff --name-only")) { + return "file1.js\nfile2.ts"; + } + return ""; + }); + + mockInquirer({ target: "main" }); + + const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); + + const { MergeManager } = await import("@/managers"); + + await MergeManager.mergeBranch(); + + expect(spy).toHaveBeenCalledWith("git merge main"); + // DO NOT continue to success message + }); + + // -------------------------------------------------------------- + // 2️⃣ showConflicts() + // -------------------------------------------------------------- + it("should show no conflicts when none exist", async () => { + (execSync as any).mockImplementation((cmd: string) => { + if (cmd.includes("git diff --name-only --diff-filter=U")) { + return ""; + } + return ""; + }); + + const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); + + const { MergeManager } = await import("@/managers"); + + await MergeManager.showConflicts(); + + expect(spy).not.toHaveBeenCalled(); // no git calls + }); + + it("should show conflict files when they exist", async () => { + (execSync as any).mockImplementation((cmd: string) => { + if (cmd.includes("git diff --name-only --diff-filter=U")) { + return "a.txt\nb.txt"; + } + return ""; + }); + + const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); + + const { MergeManager } = await import("@/managers"); + + await MergeManager.showConflicts(); + + expect(spy).not.toHaveBeenCalled(); // only prints, no git commands + }); + + // -------------------------------------------------------------- + // 3️⃣ abortMerge() + // -------------------------------------------------------------- + it("should NOT abort merge when no merge is active", async () => { + (fs.existsSync as any).mockImplementation((path: string) => { + return false; // no merge active + }); + + const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); + const { MergeManager } = await import("@/managers"); + + await MergeManager.abortMerge(); + + expect(spy).not.toHaveBeenCalled(); + }); + + it("should cancel abort when user selects NO", async () => { + fs.existsSync.mockImplementation((path: string) => { + return path.includes("MERGE_HEAD"); + }); + + mockInquirer({ confirm: false }); + + const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); + + const { MergeManager } = await import("@/managers"); + + await MergeManager.abortMerge(); + + expect(spy).not.toHaveBeenCalled(); + }); + + it("should abort merge when confirmed", async () => { + // Reset modules to get fresh import with updated mock + vi.resetModules(); + + // Re-import fs to get the mocked version + const fsMocked = await import("fs"); + (fsMocked.existsSync as any).mockImplementation((path: string) => { + return typeof path === 'string' && path.includes("MERGE_HEAD"); + }); + + mockInquirer({ confirm: true }); + + // Re-import GitExecutor after reset and set up spy + const { GitExecutor: GitExecutorAfterReset } = await import("@/core/GitExecutor"); + const spy = vi.spyOn(GitExecutorAfterReset, "run").mockResolvedValue(); + + const { MergeManager } = await import("@/managers"); + + await MergeManager.abortMerge(); + + expect(spy).toHaveBeenCalledWith("git merge --abort"); + }); + + // -------------------------------------------------------------- + // 4️⃣ continueMerge() + // -------------------------------------------------------------- + it("should not continue merge when merge not active", async () => { + fs.existsSync.mockImplementation((path: string) => { + return false; // no merge active + }); + + const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); + + const { MergeManager } = await import("@/managers"); + + await MergeManager.continueMerge(); + + expect(spy).not.toHaveBeenCalled(); + }); + + it("should continue merge when merge is active", async () => { + // Reset modules to get fresh import with updated mock + vi.resetModules(); + + // Re-import fs to get the mocked version + const fsMocked = await import("fs"); + (fsMocked.existsSync as any).mockImplementation((path: string) => { + return typeof path === 'string' && path.includes("MERGE_HEAD"); + }); + + // Re-import GitExecutor after reset and set up spy + const { GitExecutor: GitExecutorAfterReset } = await import("@/core/GitExecutor"); + const spy = vi.spyOn(GitExecutorAfterReset, "run").mockResolvedValue(); + + const { MergeManager } = await import("@/managers"); + + await MergeManager.continueMerge(); + + expect(spy).toHaveBeenCalledWith("git merge --continue"); + }); +}); diff --git a/tests/unit/RebaseManager.test.ts b/tests/unit/RebaseManager.test.ts new file mode 100644 index 0000000..1ff5f15 --- /dev/null +++ b/tests/unit/RebaseManager.test.ts @@ -0,0 +1,275 @@ +import { describe, it, expect, beforeEach, vi } from "vitest"; +import { mockInquirer } from "../mocks/inquirerMock"; +import { GitExecutor } from "@/core/GitExecutor"; + +/** + * Properly mock fs by importing actual and overriding existsSync only. + * Node's fs is a CommonJS module, so we need to mock both default and named exports. + */ +vi.mock("fs", async () => { + const actual = await vi.importActual("fs"); + const mockExistsSync = vi.fn(() => false); + return { + ...actual, + default: { + ...actual, + existsSync: mockExistsSync, + }, + existsSync: mockExistsSync, + }; +}); + +vi.mock('child_process', async (importOriginal) => { + const actual: any = await importOriginal(); + return { + ...(actual || {}), + execSync: vi.fn(), + // ensure `exec` is available for modules that import it (GitExecutor) + exec: actual?.exec ?? vi.fn(), + // Mock spawn for GitExecutor.run + spawn: vi.fn(), + } as any; +}); + +import * as fs from "fs"; +import { execSync } from "child_process"; + +describe("RebaseManager – Full Test Suite", () => { + beforeEach(() => { + vi.clearAllMocks(); + // Reset fs.existsSync mock to default (false) + (fs.existsSync as unknown as vi.Mock).mockReturnValue(false); + }); + + // ------------------------------------------------------------- + // startRebase() + // ------------------------------------------------------------- + it("blocks start when a rebase is already in progress", async () => { + (fs.existsSync as unknown as vi.Mock).mockImplementation((path: string) => { + return typeof path === 'string' && path.includes("REBASE_HEAD"); + }); + const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); + + const { RebaseManager } = await import("@/managers"); + await RebaseManager.startRebase(); + + expect(spy).not.toHaveBeenCalled(); + }); + + it("does nothing when no branches are available", async () => { + (fs.existsSync as unknown as vi.Mock).mockReturnValue(false); + (execSync as unknown as vi.Mock).mockReturnValue(""); // no branches + + const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); + const { RebaseManager } = await import("@/managers"); + + await RebaseManager.startRebase(); + + expect(spy).not.toHaveBeenCalled(); + }); + + it("rebases onto selected branch when no conflicts", async () => { + (fs.existsSync as unknown as vi.Mock).mockReturnValue(false); + + // git branch --all returns list; git diff has no conflicts + (execSync as unknown as vi.Mock).mockImplementation((cmd: string) => { + if (cmd.includes("git branch --all")) { + return "* main\n feature\n develop\n"; + } + if (cmd.includes("git diff --name-only")) { + return ""; // no conflicts + } + return ""; + }); + + mockInquirer({ onto: "feature" }); + const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); + + const { RebaseManager } = await import("@/managers"); + await RebaseManager.startRebase(); + + expect(spy).toHaveBeenCalledWith("git rebase feature"); + }); + + it("prints conflicts when rebase produces conflicts", async () => { + (fs.existsSync as unknown as vi.Mock).mockReturnValue(false); + + (execSync as unknown as vi.Mock).mockImplementation((cmd: string) => { + if (cmd.includes("git branch --all")) { + return "main\nfeature"; + } + if (cmd.includes("git diff --name-only")) { + return "file1.js\nfile2.ts"; + } + return ""; + }); + + mockInquirer({ onto: "main" }); + const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); + + const { RebaseManager } = await import("@/managers"); + await RebaseManager.startRebase(); + + expect(spy).toHaveBeenCalledWith("git rebase main"); + // conflicts cause early return — we asserted the merge command ran + }); + + // ------------------------------------------------------------- + // interactiveRebase() + // ------------------------------------------------------------- + it("starts interactive rebase with provided count", async () => { + mockInquirer({ count: "3" }); + + const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); + const { RebaseManager } = await import("@/managers"); + + await RebaseManager.interactiveRebase(); + + expect(spy).toHaveBeenCalledWith("git rebase -i HEAD~3"); + }); + + // ------------------------------------------------------------- + // continueRebase() + // ------------------------------------------------------------- + it("does nothing when no rebase in progress for continue", async () => { + (fs.existsSync as unknown as vi.Mock).mockReturnValue(false); + + const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); + const { RebaseManager } = await import("@/managers"); + + await RebaseManager.continueRebase(); + + expect(spy).not.toHaveBeenCalled(); + }); + + it("continues rebase when in progress", async () => { + // Reset modules to get fresh import with updated mock + vi.resetModules(); + + // Re-import fs to get the mocked version + const fsMocked = await import("fs"); + (fsMocked.existsSync as any).mockImplementation((path: string) => { + return typeof path === 'string' && path.includes("REBASE_HEAD"); + }); + + // Re-import GitExecutor after reset and set up spy + const { GitExecutor: GitExecutorAfterReset } = await import("@/core/GitExecutor"); + const spy = vi.spyOn(GitExecutorAfterReset, "run").mockResolvedValue(); + + const { RebaseManager } = await import("@/managers"); + + await RebaseManager.continueRebase(); + + expect(spy).toHaveBeenCalledWith("git rebase --continue"); + }); + + // ------------------------------------------------------------- + // skipCommit() + // ------------------------------------------------------------- + it("does nothing when no rebase in progress for skip", async () => { + (fs.existsSync as unknown as vi.Mock).mockReturnValue(false); + + const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); + const { RebaseManager } = await import("@/managers"); + + await RebaseManager.skipCommit(); + + expect(spy).not.toHaveBeenCalled(); + }); + + it("skips commit when rebase is active", async () => { + // Reset modules to get fresh import with updated mock + vi.resetModules(); + + // Re-import fs to get the mocked version + const fsMocked = await import("fs"); + (fsMocked.existsSync as any).mockImplementation((path: string) => { + return typeof path === 'string' && path.includes("REBASE_HEAD"); + }); + + // Re-import GitExecutor after reset and set up spy + const { GitExecutor: GitExecutorAfterReset } = await import("@/core/GitExecutor"); + const spy = vi.spyOn(GitExecutorAfterReset, "run").mockResolvedValue(); + + const { RebaseManager } = await import("@/managers"); + + await RebaseManager.skipCommit(); + + expect(spy).toHaveBeenCalledWith("git rebase --skip"); + }); + + // ------------------------------------------------------------- + // abortRebase() + // ------------------------------------------------------------- + it("does nothing when no rebase in progress for abort", async () => { + (fs.existsSync as unknown as vi.Mock).mockReturnValue(false); + + const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); + const { RebaseManager } = await import("@/managers"); + + await RebaseManager.abortRebase(); + + expect(spy).not.toHaveBeenCalled(); + }); + + it("cancels abort when user rejects", async () => { + (fs.existsSync as unknown as vi.Mock).mockReturnValue(true); + + mockInquirer({ confirm: false }); + + const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); + const { RebaseManager } = await import("@/managers"); + + await RebaseManager.abortRebase(); + + expect(spy).not.toHaveBeenCalled(); + }); + + it("aborts rebase when confirmed", async () => { + // Reset modules to get fresh import with updated mock + vi.resetModules(); + + // Re-import fs to get the mocked version + const fsMocked = await import("fs"); + (fsMocked.existsSync as any).mockImplementation((path: string) => { + return typeof path === 'string' && path.includes("REBASE_HEAD"); + }); + + mockInquirer({ confirm: true }); + + // Re-import GitExecutor after reset and set up spy + const { GitExecutor: GitExecutorAfterReset } = await import("@/core/GitExecutor"); + const spy = vi.spyOn(GitExecutorAfterReset, "run").mockResolvedValue(); + + const { RebaseManager } = await import("@/managers"); + + await RebaseManager.abortRebase(); + + expect(spy).toHaveBeenCalledWith("git rebase --abort"); + }); + + // ------------------------------------------------------------- + // showConflicts() + // ------------------------------------------------------------- + it("reports no conflicts when none exist", async () => { + (execSync as unknown as vi.Mock).mockReturnValue(""); + + const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); + const { RebaseManager } = await import("@/managers"); + + await RebaseManager.showConflicts(); + + expect(spy).not.toHaveBeenCalled(); + }); + + it("shows conflict files when they exist", async () => { + (execSync as unknown as vi.Mock).mockReturnValue("a.txt\nb.txt"); + + const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); + const { RebaseManager } = await import("@/managers"); + + await RebaseManager.showConflicts(); + + expect(spy).not.toHaveBeenCalled(); // showConflicts only prints conflicts + }); +}); diff --git a/tests/unit/RemoteManager.test.ts b/tests/unit/RemoteManager.test.ts new file mode 100644 index 0000000..1f2adf9 --- /dev/null +++ b/tests/unit/RemoteManager.test.ts @@ -0,0 +1,265 @@ +import { describe, it, expect, beforeEach, vi } from "vitest"; +import { GitExecutor } from "@/core/GitExecutor"; +import { mockInquirer } from "../mocks/inquirerMock"; + +// Mock execSync BEFORE importing RemoteManager +vi.mock('child_process', async (importOriginal) => { + const actual: any = await importOriginal(); + return { + ...(actual || {}), + execSync: vi.fn(), + // ensure `exec` is available for modules that import it (GitExecutor) + exec: actual?.exec ?? vi.fn(), + } as any; +}); + +import { execSync } from "child_process"; + +describe("RemoteManager – Full Test Suite", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + // -------------------------------------------------------------- + // 1️⃣ listRemotes() + // -------------------------------------------------------------- + it("should list remotes", async () => { + const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); + + const { RemoteManager } = await import("@/managers"); + + await RemoteManager.listRemotes(); + + expect(spy).toHaveBeenCalledWith("git remote -v"); + }); + + // -------------------------------------------------------------- + // 2️⃣ addRemote() + // -------------------------------------------------------------- + it("should add a remote", async () => { + mockInquirer({ name: "origin", url: "https://github.com/test/repo" }); + + const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); + + const { RemoteManager } = await import("@/managers"); + + await RemoteManager.addRemote(); + + expect(spy).toHaveBeenCalledWith( + "git remote add origin https://github.com/test/repo" + ); + }); + + // -------------------------------------------------------------- + // 3️⃣ renameRemote() + // -------------------------------------------------------------- + it("should rename a remote", async () => { + (execSync as any).mockReturnValue("origin\nbackup"); + + mockInquirer({ oldName: "origin" }); + mockInquirer({ newName: "main-origin" }); + + const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); + + const { RemoteManager } = await import("@/managers"); + + await RemoteManager.renameRemote(); + + expect(spy).toHaveBeenCalledWith("git remote rename origin main-origin"); + }); + + it("should not rename if no remotes exist", async () => { + (execSync as any).mockReturnValue(""); + + const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); + + const { RemoteManager } = await import("@/managers"); + + await RemoteManager.renameRemote(); + + expect(spy).not.toHaveBeenCalled(); + }); + + // -------------------------------------------------------------- + // 4️⃣ removeRemote() + // -------------------------------------------------------------- + it("should remove remote", async () => { + (execSync as any).mockReturnValue("origin\nbackup"); + + mockInquirer({ name: "backup" }); + + const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); + + const { RemoteManager } = await import("@/managers"); + + await RemoteManager.removeRemote(); + + expect(spy).toHaveBeenCalledWith("git remote remove backup"); + }); + + it("should not remove if no remotes", async () => { + (execSync as any).mockReturnValue(""); + + const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); + + const { RemoteManager } = await import("@/managers"); + + await RemoteManager.removeRemote(); + + expect(spy).not.toHaveBeenCalled(); + }); + + // -------------------------------------------------------------- + // 5️⃣ updateRemoteUrl() + // -------------------------------------------------------------- + it("should update remote URL", async () => { + (execSync as any).mockReturnValue("origin"); + + mockInquirer({ name: "origin" }); + mockInquirer({ url: "https://github.com/new/url" }); + + const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); + + const { RemoteManager } = await import("@/managers"); + + await RemoteManager.updateRemoteUrl(); + + expect(spy).toHaveBeenCalledWith( + "git remote set-url origin https://github.com/new/url" + ); + }); + + // -------------------------------------------------------------- + // 6️⃣ pushChanges() + // -------------------------------------------------------------- + it("should push changes to selected remote", async () => { + (execSync as any).mockReturnValue("origin\nbackup"); + + mockInquirer({ remote: "backup" }); + + const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); + + const { RemoteManager } = await import("@/managers"); + + await RemoteManager.pushChanges(); + + expect(spy).toHaveBeenCalledWith("git push backup"); + }); + + // If no remotes → default to origin + it("should push to origin when no remotes exist", async () => { + (execSync as any).mockReturnValue(""); + + mockInquirer({ remote: "origin" }); + + const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); + const { RemoteManager } = await import("@/managers"); + + await RemoteManager.pushChanges(); + + expect(spy).toHaveBeenCalledWith("git push origin"); + }); + + // -------------------------------------------------------------- + // 7️⃣ pushWithUpstream() + // -------------------------------------------------------------- + it("should push with upstream", async () => { + (execSync as any).mockReturnValue("origin"); + + mockInquirer({ remote: "origin" }); + mockInquirer({ branch: "dev" }); + + const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); + + const { RemoteManager } = await import("@/managers"); + + await RemoteManager.pushWithUpstream(); + + expect(spy).toHaveBeenCalledWith("git push -u origin dev"); + }); + + // -------------------------------------------------------------- + // 8️⃣ pullChanges() + // -------------------------------------------------------------- + it("should pull from selected remote", async () => { + (execSync as any).mockReturnValue("origin\nteam"); + + mockInquirer({ remote: "team" }); + + const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); + const { RemoteManager } = await import("@/managers"); + + await RemoteManager.pullChanges(); + + expect(spy).toHaveBeenCalledWith("git pull team"); + }); + + // -------------------------------------------------------------- + // 9️⃣ fetchUpdates() + // -------------------------------------------------------------- + it("should fetch from selected remote", async () => { + (execSync as any).mockReturnValue("origin\ntest"); + + mockInquirer({ remote: "test" }); + + const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); + const { RemoteManager } = await import("@/managers"); + + await RemoteManager.fetchUpdates(); + + expect(spy).toHaveBeenCalledWith("git fetch test"); + }); + + it("should fetch --all when user selects 'all'", async () => { + (execSync as any).mockReturnValue("origin\nteam"); + + mockInquirer({ remote: "all" }); + + const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); + const { RemoteManager } = await import("@/managers"); + + await RemoteManager.fetchUpdates(); + + expect(spy).toHaveBeenCalledWith("git fetch --all"); + }); + + // -------------------------------------------------------------- + // 🔟 showRemoteInfo() + // -------------------------------------------------------------- + it("should show remote info", async () => { + (execSync as any).mockReturnValue("origin\nbackup"); + + mockInquirer({ remote: "backup" }); + + const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); + const { RemoteManager } = await import("@/managers"); + + await RemoteManager.showRemoteInfo(); + + expect(spy).toHaveBeenCalledWith("git remote show backup"); + }); + + it("should do nothing if no remotes exist", async () => { + (execSync as any).mockReturnValue(""); + + const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); + const { RemoteManager } = await import("@/managers"); + + await RemoteManager.showRemoteInfo(); + + expect(spy).not.toHaveBeenCalled(); + }); + + // -------------------------------------------------------------- + // 1️⃣1️⃣ syncAll() + // -------------------------------------------------------------- + it("should sync all remotes", async () => { + const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); + + const { RemoteManager } = await import("@/managers"); + + await RemoteManager.syncAll(); + + expect(spy).toHaveBeenCalledWith("git fetch --all && git pull --all"); + }); +}); diff --git a/tests/unit/RepositoryManager.test.ts b/tests/unit/RepositoryManager.test.ts new file mode 100644 index 0000000..2d9593a --- /dev/null +++ b/tests/unit/RepositoryManager.test.ts @@ -0,0 +1,260 @@ + +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { RepositoryManager } from "@/managers"; +import { GitExecutor } from "@/core/GitExecutor"; +import { mockInquirer } from "../mocks/inquirerMock"; +import { createTestRepo } from "../helpers/testRepo"; + +describe("RepositoryManager - Full Test Suite", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + // ------------------------------------------------------------- + // 🏗 initRepo() + // ------------------------------------------------------------- + it("should initialize a standard repo", async () => { + const repo = createTestRepo(); + process.chdir(repo); + + mockInquirer({ bare: false }); + + const {RepositoryManager} = await import("@/managers") + + const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); + + await RepositoryManager.initRepo(); + + expect(spy).toHaveBeenCalledWith("git init"); + }); + + it("should initialize a bare repo", async () => { + const repo = createTestRepo(); + process.chdir(repo); + + mockInquirer({ bare: true }); + + const {RepositoryManager} = await import("@/managers") + + const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); + + await RepositoryManager.initRepo(); + + expect(spy).toHaveBeenCalledWith("git init --bare"); + }); + + // ------------------------------------------------------------- + // 🌐 cloneRepo() + // ------------------------------------------------------------- + it("should clone repo without folder", async () => { + mockInquirer({ + url: "https://github.com/aditya/repo", + folder: "" + }); + + const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); + + await RepositoryManager.cloneRepo(); + + expect(spy).toHaveBeenCalledWith("git clone https://github.com/aditya/repo"); + }); + + it("should clone repo with target folder", async () => { + mockInquirer({ + url: "https://github.com/aditya/repo", + folder: "my-folder" + }); + + const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); + + await RepositoryManager.cloneRepo(); + + expect(spy).toHaveBeenCalledWith("git clone https://github.com/aditya/repo my-folder"); + }); + + // ------------------------------------------------------------- + // 📂 status() + // ------------------------------------------------------------- + it("should run short status", async () => { + mockInquirer({ short: true }); + + const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); + + await RepositoryManager.status(); + + expect(spy).toHaveBeenCalledWith("git status -s"); + }); + + it("should run full status", async () => { + mockInquirer({ short: false }); + + const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); + + await RepositoryManager.status(); + + expect(spy).toHaveBeenCalledWith("git status"); + }); + + // ------------------------------------------------------------- + // ⚙️ showConfig() + // ------------------------------------------------------------- + it("should show local config", async () => { + mockInquirer({ scope: "--local" }); + + const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); + + await RepositoryManager.showConfig(); + + expect(spy).toHaveBeenCalledWith("git config --local --list"); + }); + + it("should show global config", async () => { + mockInquirer({ scope: "--global" }); + + const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); + + await RepositoryManager.showConfig(); + + expect(spy).toHaveBeenCalledWith("git config --global --list"); + }); + + it("should show system config", async () => { + mockInquirer({ scope: "--system" }); + + const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); + + await RepositoryManager.showConfig(); + + expect(spy).toHaveBeenCalledWith("git config --system --list"); + }); + + it("should list all configs", async () => { + mockInquirer({ scope: "--list" }); + + const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); + + await RepositoryManager.showConfig(); + + expect(spy).toHaveBeenCalledWith("git config --list"); + }); + + // ------------------------------------------------------------- + // ✏️ setConfig() + // ------------------------------------------------------------- + it("should set local config", async () => { + mockInquirer({ + key: "user.name", + value: "Aditya", + global: false + }); + + const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); + + await RepositoryManager.setConfig(); + + expect(spy).toHaveBeenCalledWith(`git config user.name "Aditya"`); + }); + + it("should set global config", async () => { + mockInquirer({ + key: "user.email", + value: "test@example.com", + global: true + }); + + const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); + + await RepositoryManager.setConfig(); + + expect(spy).toHaveBeenCalledWith(`git config --global user.email "test@example.com"`); + }); + + // ------------------------------------------------------------- + // 🔗 listRemotes() + // ------------------------------------------------------------- + it("should list remotes", async () => { + const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); + + await RepositoryManager.listRemotes(); + + expect(spy).toHaveBeenCalledWith("git remote -v"); + }); + + // ------------------------------------------------------------- + // ➕ addRemote() + // ------------------------------------------------------------- + it("should add a remote", async () => { + mockInquirer({ + name: "origin", + url: "https://github.com/aditya/plain-git" + }); + + const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); + + await RepositoryManager.addRemote(); + + expect(spy).toHaveBeenCalledWith("git remote add origin https://github.com/aditya/plain-git"); + }); + + // ------------------------------------------------------------- + // ✏️ updateRemote() + // ------------------------------------------------------------- + it("should update a remote", async () => { + mockInquirer({ + name: "origin", + url: "https://github.com/aditya/new-url" + }); + + const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); + + await RepositoryManager.updateRemote(); + + expect(spy).toHaveBeenCalledWith("git remote set-url origin https://github.com/aditya/new-url"); + }); + + // ------------------------------------------------------------- + // 🗑️ removeRemote() + // ------------------------------------------------------------- + it("should remove a remote", async () => { + mockInquirer({ name: "origin" }); + + const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); + + await RepositoryManager.removeRemote(); + + expect(spy).toHaveBeenCalledWith("git remote remove origin"); + }); + + // ------------------------------------------------------------- + // 🧠 showRepoInfo() + // ------------------------------------------------------------- + it("should show repo info", async () => { + const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); + + await RepositoryManager.showRepoInfo(); + + expect(spy).toHaveBeenCalledWith("git rev-parse --show-toplevel && git rev-parse --abbrev-ref HEAD"); + }); + + // ------------------------------------------------------------- + // 🧹 optimizeRepo() + // ------------------------------------------------------------- + it("should optimize the repo", async () => { + const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); + + await RepositoryManager.optimizeRepo(); + + expect(spy).toHaveBeenCalledWith("git gc --prune=now --aggressive"); + }); + + // ------------------------------------------------------------- + // 🧾 verifyRepo() + // ------------------------------------------------------------- + it("should verify repo integrity", async () => { + const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); + + await RepositoryManager.verifyRepo(); + + expect(spy).toHaveBeenCalledWith("git fsck --full --progress"); + }); +}); diff --git a/tests/unit/ResetManager.test.ts b/tests/unit/ResetManager.test.ts new file mode 100644 index 0000000..5d80397 --- /dev/null +++ b/tests/unit/ResetManager.test.ts @@ -0,0 +1,244 @@ +import { describe, it, expect, beforeEach, vi } from "vitest"; +import { mockInquirer } from "@tests/mocks/inquirerMock"; +import { GitExecutor } from "@/core/GitExecutor"; + +// Mock child_process.execSync before importing the manager +vi.mock('child_process', async (importOriginal) => { + const actual: any = await importOriginal(); + return { + ...(actual || {}), + execSync: vi.fn(), + // ensure `exec` is available for modules that import it (GitExecutor) + exec: actual?.exec ?? vi.fn(), + // Mock spawn for GitExecutor.run + spawn: vi.fn(), + } as any; +}); + +import { execSync } from "child_process"; + +describe("ResetManager – Full Test Suite", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + // ------------------------------------------------------------- + // undoLastCommitSoft() + // ------------------------------------------------------------- + it("should perform a soft undo of last commit", async () => { + const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); + + const { ResetManager } = await import("@/managers"); + await ResetManager.undoLastCommitSoft(); + + expect(spy).toHaveBeenCalledWith("git reset --soft HEAD~1"); + }); + + // ------------------------------------------------------------- + // undoLastCommitMixed() + // ------------------------------------------------------------- + it("should perform a mixed undo of last commit", async () => { + const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); + + const { ResetManager } = await import("@/managers"); + await ResetManager.undoLastCommitMixed(); + + expect(spy).toHaveBeenCalledWith("git reset --mixed HEAD~1"); + }); + + // ------------------------------------------------------------- + // undoLastCommitHard() + // ------------------------------------------------------------- + it("should cancel hard undo when user rejects confirmation", async () => { + mockInquirer({ confirm: false }); + + const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); + + const { ResetManager } = await import("@/managers"); + await ResetManager.undoLastCommitHard(); + + expect(spy).not.toHaveBeenCalled(); + }); + + it("should perform hard undo when user confirms", async () => { + mockInquirer({ confirm: true }); + + const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); + + const { ResetManager } = await import("@/managers"); + await ResetManager.undoLastCommitHard(); + + expect(spy).toHaveBeenCalledWith("git reset --hard HEAD~1"); + }); + + // ------------------------------------------------------------- + // resetToSpecificCommit() + // ------------------------------------------------------------- + it("should do nothing when no recent commits exist", async () => { + (execSync as unknown as vi.Mock).mockReturnValue(""); + + const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); + + const { ResetManager } = await import("@/managers"); + await ResetManager.resetToSpecificCommit(); + + expect(spy).not.toHaveBeenCalled(); + }); + + it("should reset to chosen commit with chosen mode", async () => { + // simulate git log --oneline output + (execSync as unknown as vi.Mock).mockReturnValue("abc123 First\nbcd234 Second\n"); + + mockInquirer({ commit: "bcd234 Second" }); + mockInquirer({ mode: "--mixed" }); + + const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); + + const { ResetManager } = await import("@/managers"); + await ResetManager.resetToSpecificCommit(); + + expect(spy).toHaveBeenCalledWith("git reset --mixed bcd234"); + }); + + // ------------------------------------------------------------- + // discardFileChanges() + // ------------------------------------------------------------- + it("should do nothing when no modified files exist", async () => { + (execSync as unknown as vi.Mock).mockReturnValue(""); + + const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); + + const { ResetManager } = await import("@/managers"); + await ResetManager.discardFileChanges(); + + expect(spy).not.toHaveBeenCalled(); + }); + + it("should not discard when user selects none", async () => { + (execSync as unknown as vi.Mock).mockReturnValue(" M a.js\n M b.js"); + + mockInquirer({ selected: [] }); // user selects none + + const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); + + const { ResetManager } = await import("@/managers"); + await ResetManager.discardFileChanges(); + + expect(spy).not.toHaveBeenCalled(); + }); + + it("should discard selected files when confirmed", async () => { + (execSync as unknown as vi.Mock).mockReturnValue(" M a.js\n M b.js"); + + mockInquirer({ selected: ["a.js", "b.js"] }); + mockInquirer({ confirm: true }); + + const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); + + const { ResetManager } = await import("@/managers"); + await ResetManager.discardFileChanges(); + + expect(spy).toHaveBeenCalledWith("git restore a.js"); + expect(spy).toHaveBeenCalledWith("git restore b.js"); + }); + + // ------------------------------------------------------------- + // cleanUntracked() + // ------------------------------------------------------------- + it("should cancel cleaning when user rejects", async () => { + mockInquirer({ confirm: false }); + + const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); + + const { ResetManager } = await import("@/managers"); + await ResetManager.cleanUntracked(); + + expect(spy).not.toHaveBeenCalled(); + }); + + it("should clean untracked files when confirmed", async () => { + mockInquirer({ confirm: true }); + + const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); + + const { ResetManager } = await import("@/managers"); + await ResetManager.cleanUntracked(); + + expect(spy).toHaveBeenCalledWith("git clean -fd"); + }); + + // ------------------------------------------------------------- + // interactiveReset() + // ------------------------------------------------------------- + it("should call undoLastCommitSoft when mode is soft", async () => { + mockInquirer({ mode: "soft" }); + + const { ResetManager } = await import("@/managers"); + + const softSpy = vi.spyOn(ResetManager, "undoLastCommitSoft").mockResolvedValue(); + + await ResetManager.interactiveReset(); + + expect(softSpy).toHaveBeenCalled(); + }); + + it("should call undoLastCommitMixed when mode is mixed", async () => { + mockInquirer({ mode: "mixed" }); + + const { ResetManager } = await import("@/managers"); + + const mixedSpy = vi.spyOn(ResetManager, "undoLastCommitMixed").mockResolvedValue(); + + await ResetManager.interactiveReset(); + + expect(mixedSpy).toHaveBeenCalled(); + }); + + it("should call undoLastCommitHard when mode is hard", async () => { + mockInquirer({ mode: "hard" }); + + const { ResetManager } = await import("@/managers"); + + const hardSpy = vi.spyOn(ResetManager, "undoLastCommitHard").mockResolvedValue(); + + await ResetManager.interactiveReset(); + + expect(hardSpy).toHaveBeenCalled(); + }); + + it("should call resetToSpecificCommit when mode is specific", async () => { + mockInquirer({ mode: "specific" }); + + const { ResetManager } = await import("@/managers"); + + const specSpy = vi.spyOn(ResetManager, "resetToSpecificCommit").mockResolvedValue(); + + await ResetManager.interactiveReset(); + + expect(specSpy).toHaveBeenCalled(); + }); + + it("should call discardFileChanges when mode is discard", async () => { + mockInquirer({ mode: "discard" }); + + const { ResetManager } = await import("@/managers"); + + const discSpy = vi.spyOn(ResetManager, "discardFileChanges").mockResolvedValue(); + + await ResetManager.interactiveReset(); + + expect(discSpy).toHaveBeenCalled(); + }); + + it("should call cleanUntracked when mode is clean", async () => { + mockInquirer({ mode: "clean" }); + + const { ResetManager } = await import("@/managers"); + + const cleanSpy = vi.spyOn(ResetManager, "cleanUntracked").mockResolvedValue(); + + await ResetManager.interactiveReset(); + + expect(cleanSpy).toHaveBeenCalled(); + }); +}); diff --git a/tests/unit/StashManager.test.ts b/tests/unit/StashManager.test.ts new file mode 100644 index 0000000..b8de3b8 --- /dev/null +++ b/tests/unit/StashManager.test.ts @@ -0,0 +1,195 @@ +import { describe, it, expect, beforeEach, vi } from "vitest"; +import { GitExecutor } from "@/core/GitExecutor"; +import { mockInquirer } from "../mocks/inquirerMock"; + +// Mock execSync BEFORE importing StashManager +vi.mock('child_process', async (importOriginal) => { + const actual: any = await importOriginal(); + return { + ...(actual || {}), + execSync: vi.fn(), + // ensure `exec` is available for modules that import it (GitExecutor) + exec: actual?.exec ?? vi.fn(), + } as any; +}); + + +import { execSync } from "child_process"; + +describe("StashManager – Full Suite", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + // ---------------------------------------------------------- + // 1️⃣ createStash() + // ---------------------------------------------------------- + it("should create stash WITHOUT message", async () => { + mockInquirer({ message: " " }); // empty message + + const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); + + const { StashManager } = await import("@/managers"); + + await StashManager.createStash(); + + expect(spy).toHaveBeenCalledWith("git stash push"); + }); + + it("should create stash WITH message", async () => { + mockInquirer({ message: "My stash" }); + + const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); + + const { StashManager } = await import("@/managers"); + + await StashManager.createStash(); + + expect(spy).toHaveBeenCalledWith(`git stash push -m "My stash"`); + }); + + // ---------------------------------------------------------- + // 2️⃣ listStashes() + // ---------------------------------------------------------- + it("should list stash entries", async () => { + const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); + + const { StashManager } = await import("@/managers"); + + await StashManager.listStashes(); + + expect(spy).toHaveBeenCalledWith("git stash list"); + }); + + // ---------------------------------------------------------- + // 3️⃣ applyStash() + // ---------------------------------------------------------- + it("should NOT apply stash if none exist", async () => { + (execSync as any).mockReturnValue(""); + + const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); + + const { StashManager } = await import("@/managers"); + + await StashManager.applyStash(); + + expect(spy).not.toHaveBeenCalled(); + }); + + it("should apply selected stash", async () => { + (execSync as any).mockReturnValue( + "stash@{0}: WIP on main: abc123 first stash\nstash@{1}: WIP on dev: xyz987 second stash" + ); + + mockInquirer({ selected: "stash@{1}" }); + + const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); + + const { StashManager } = await import("@/managers"); + + await StashManager.applyStash(); + + expect(spy).toHaveBeenCalledWith("git stash apply stash@{1}"); + }); + + // ---------------------------------------------------------- + // 4️⃣ popStash() + // ---------------------------------------------------------- + it("should NOT pop stash if none exist", async () => { + (execSync as any).mockReturnValue(""); + + const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); + + const { StashManager } = await import("@/managers"); + + await StashManager.popStash(); + + expect(spy).not.toHaveBeenCalled(); + }); + + it("should pop selected stash", async () => { + (execSync as any).mockReturnValue("stash@{0}: WIP on main: initial stash"); + + mockInquirer({ selected: "stash@{0}" }); + + const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); + + const { StashManager } = await import("@/managers"); + + await StashManager.popStash(); + + expect(spy).toHaveBeenCalledWith("git stash pop stash@{0}"); + }); + + // ---------------------------------------------------------- + // 5️⃣ dropStash() + // ---------------------------------------------------------- + it("should NOT drop if no stashes exist", async () => { + (execSync as any).mockReturnValue(""); + + const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); + + const { StashManager } = await import("@/managers"); + + await StashManager.dropStash(); + + expect(spy).not.toHaveBeenCalled(); + }); + + it("should cancel drop when confirm=false", async () => { + (execSync as any).mockReturnValue("stash@{0}: test"); + + mockInquirer({ selected: "stash@{0}" }); + mockInquirer({ confirm: false }); // user cancels + + const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); + + const { StashManager } = await import("@/managers"); + + await StashManager.dropStash(); + + expect(spy).not.toHaveBeenCalled(); + }); + + it("should drop stash when confirmed", async () => { + (execSync as any).mockReturnValue("stash@{0}: test stash"); + + mockInquirer({ selected: "stash@{0}" }); + mockInquirer({ confirm: true }); + + const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); + + const { StashManager } = await import("@/managers"); + + await StashManager.dropStash(); + + expect(spy).toHaveBeenCalledWith("git stash drop stash@{0}"); + }); + + // ---------------------------------------------------------- + // 6️⃣ clearStashes() + // ---------------------------------------------------------- + it("should NOT clear stashes when user cancels", async () => { + mockInquirer({ confirm: false }); + + const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); + + const { StashManager } = await import("@/managers"); + + await StashManager.clearStashes(); + + expect(spy).not.toHaveBeenCalled(); + }); + + it("should clear all stashes when confirmed", async () => { + mockInquirer({ confirm: true }); + + const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); + + const { StashManager } = await import("@/managers"); + + await StashManager.clearStashes(); + + expect(spy).toHaveBeenCalledWith("git stash clear"); + }); +}); diff --git a/tests/unit/TagManager.test.ts b/tests/unit/TagManager.test.ts new file mode 100644 index 0000000..0467d63 --- /dev/null +++ b/tests/unit/TagManager.test.ts @@ -0,0 +1,175 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { GitExecutor } from '@/core/GitExecutor'; +import { mockInquirer } from '../mocks/inquirerMock'; + +// Mock execSync BEFORE importing TagManager +vi.mock('child_process', async (importOriginal) => { + const actual: any = await importOriginal(); + return { + ...(actual || {}), + execSync: vi.fn(), + // ensure `exec` is available for modules that import it (GitExecutor) + exec: actual?.exec ?? vi.fn(), + } as any; +}); + +import { execSync } from 'child_process'; +import type { Mock } from 'vitest'; + +describe('TagManager – Full Test Suite', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + // -------------------------------------------------------------- + // 1️⃣ listTags() + // -------------------------------------------------------------- + it('should list all tags', async () => { + const spy = vi.spyOn(GitExecutor, 'run').mockResolvedValue(); + + const { TagManager } = await import('@/managers'); + + await TagManager.listTags(); + + expect(spy).toHaveBeenCalledWith('git tag --list'); + }); + + // -------------------------------------------------------------- + // 2️⃣ createTag() + // -------------------------------------------------------------- + it('should create a lightweight tag', async () => { + mockInquirer({ tagName: 'v1.0.0' }); + + const spy = vi.spyOn(GitExecutor, 'run').mockResolvedValue(); + + const { TagManager } = await import('@/managers'); + + await TagManager.createTag(); + + expect(spy).toHaveBeenCalledWith('git tag v1.0.0'); + }); + + // -------------------------------------------------------------- + // 3️⃣ createAnnotatedTag() + // -------------------------------------------------------------- + it('should create an annotated tag', async () => { + mockInquirer({ tagName: 'v1.0.0', message: 'Release version 1' }); + + const spy = vi.spyOn(GitExecutor, 'run').mockResolvedValue(); + + const { TagManager } = await import('@/managers'); + + await TagManager.createAnnotatedTag(); + + expect(spy).toHaveBeenCalledWith(`git tag -a v1.0.0 -m "Release version 1"`); + }); + + // -------------------------------------------------------------- + // 4️⃣ showTagDetails() + it('should NOT show tag details when no tags exist', async () => { + (execSync as Mock).mockReturnValue(''); + + const spy = vi.spyOn(GitExecutor, 'run').mockResolvedValue(); + + const { TagManager } = await import('@/managers'); + + await TagManager.showTagDetails(); + + expect(spy).not.toHaveBeenCalled(); + }); + it('should show details of selected tag', async () => { + (execSync as Mock).mockReturnValue('v1.0.0\nbeta\nalpha'); + + mockInquirer({ tag: 'beta' }); + + const spy = vi.spyOn(GitExecutor, 'run').mockResolvedValue(); + + const { TagManager } = await import('@/managers'); + + await TagManager.showTagDetails(); + + expect(spy).toHaveBeenCalledWith('git show beta'); + }); + + // -------------------------------------------------------------- + // 5️⃣ deleteTag() + it('should NOT delete when no tags exist', async () => { + (execSync as Mock).mockReturnValue(''); + + const spy = vi.spyOn(GitExecutor, 'run').mockResolvedValue(); + + const { TagManager } = await import('@/managers'); + + await TagManager.deleteTag(); + + expect(spy).not.toHaveBeenCalled(); + }); + it('should cancel delete when user rejects confirm', async () => { + (execSync as Mock).mockReturnValue('v1.0.0'); + + mockInquirer({ tag: 'v1.0.0' }); + mockInquirer({ confirm: false }); // cancel + + const spy = vi.spyOn(GitExecutor, 'run').mockResolvedValue(); + + const { TagManager } = await import('@/managers'); + + await TagManager.deleteTag(); + + expect(spy).not.toHaveBeenCalled(); + }); + it('should delete tag when confirmed', async () => { + (execSync as Mock).mockReturnValue('v1.0.0\nv2.0.0'); + + mockInquirer({ tag: 'v2.0.0' }); + mockInquirer({ confirm: true }); + + const spy = vi.spyOn(GitExecutor, 'run').mockResolvedValue(); + + const { TagManager } = await import('@/managers'); + + await TagManager.deleteTag(); + + expect(spy).toHaveBeenCalledWith('git tag -d v2.0.0'); + }); + + // -------------------------------------------------------------- + // 6️⃣ pushTags() + // -------------------------------------------------------------- + it('should push all tags', async () => { + const spy = vi.spyOn(GitExecutor, 'run').mockResolvedValue(); + + const { TagManager } = await import('@/managers'); + + await TagManager.pushTags(); + + expect(spy).toHaveBeenCalledWith('git push --tags'); + }); + + // -------------------------------------------------------------- + // 7️⃣ pushSingleTag() + it('should NOT push single tag when none exist', async () => { + (execSync as Mock).mockReturnValue(''); + + const spy = vi.spyOn(GitExecutor, 'run').mockResolvedValue(); + + const { TagManager } = await import('@/managers'); + + await TagManager.pushSingleTag(); + + expect(spy).not.toHaveBeenCalled(); + }); + it('should push selected tag to origin', async () => { + (execSync as Mock).mockReturnValue('v1.0.0\nbeta'); + + mockInquirer({ tag: 'beta' }); + + const spy = vi.spyOn(GitExecutor, 'run').mockResolvedValue(); + + const { TagManager } = await import('@/managers'); + + await TagManager.pushSingleTag(); + + expect(spy).toHaveBeenCalledWith('git push origin beta'); + }); +}); diff --git a/tsconfig.json b/tsconfig.json index 4d284c4..6be23b7 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,8 +9,14 @@ "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, - "resolveJsonModule": true + "resolveJsonModule": true, + "baseUrl": ".", + "paths": { + "@/*": ["src/*"], + "@tests/*": ["tests/*"] + } }, - "include": ["src/**/*"], - "exclude": ["node_modules", "tests", "dist"] + "include": ["src"], + "exclude": ["node_modules", "tests", "dist"], + } \ No newline at end of file diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..622e99c --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,20 @@ +import { defineConfig } from "vitest/config"; +import path from "path"; + +export default defineConfig({ + test: { + globals: true, + environment: "node", + setupFiles: [], + coverage: { + provider: "v8", + reporter: ["text", "json", "html"], + }, + }, + resolve: { + alias: { + "@": path.resolve(__dirname, "./src"), + "@tests": path.resolve(__dirname, "tests"), + }, + } +}); From cfe6f3a525576f8c18610aeee6ad30081cb24296 Mon Sep 17 00:00:00 2001 From: prakashaditya13 Date: Fri, 21 Nov 2025 21:08:10 +0530 Subject: [PATCH 2/7] Upd: readme, changelog and package.json --- CHANGELOG.md | Bin 1778 -> 9330 bytes README.md | 84 +++++++++++++++++++++++++++++++++++++++++++++------ package.json | 2 +- 3 files changed, 76 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eb84b71da987c4ce63a44d0050b7e43f210243b4..293a144190f25e102e948c2ceb2d2702a553a22a 100644 GIT binary patch literal 9330 zcmb7~O>bOD6^3h#gxIiSb5W#GCc+sKiA|)5aO_ORQoa;Bh^3uwyJKRvjolu{v*I_f zR#VAAgk@}uB!9-o{u`$|Nh}#>B`sFfB$n=Hp;hUy}T|j z%JcHFywTsT{Iu)JcjZxeSbkYwYvn`PFN?CLPmjuv%a7{oL3vQ#>H8zS_R6__cV$!W z7JBzYYY+7Ku_!z)zbb1w_ej4V>vyLgXs~0uQeBh_osTombRJ&aE$TkU*`4yS9O_KB z{Jqd;_#f!=nZ94>{i&#akaUM-qEDegS5CC6tK3hux~KKIB%A5)qJH}et@7PMlupW2 zR3`QFOy38eiB^uq0o~CT`nCc5=JlIBou5=W*)h?+3w^U+puZtoZ`F6lIycvArWGV- zm2ak%+x}9QbDc%LeR&}4hYjEi&DIA8l5L{>cmxZbh#Ho;(5@rBvu<$Pt5O}x*U))U zIdtVxC(gg(uvl7&&6e?6L{sL)@&hbH?1^6D?A(0KxMyt zuA0rY>#%wZY5H~nfwnJpITv?)ysupkZ}7M#FRtBux>xRL=hxSN_-iMGA4;~5RXYAI zbUIMHrd`jfEwK5TwAh!<#>Xm4*?UI#Wwn|$wimGW`|{iB$%&*!&RMnTVfkL~(coBr z=ha8}8(U%Pu}3Q=S1gDb;3AABU=J}HkRKeI>WaoI%Cf-M8n9~ zlWnnP;=$j8b?9GI+hKLi5(|6PlA#NB=zHtD#!<@i%@TWf`%rrwH?{%R!6H!My<<1= z-j)01H#!w-U)G%<2z!AZ(0f+#*q0*tZ^>$$KdaUw23iCWZ{v~be1gTnb?6(MkvOvo za%0KEx_2RYSWg+=iT|E7#Rfqkke_ma1ZN&10TiI6vjUNYU5JkG)+g~J?%J&JR(W$Q ziY*JcACHv3DN@!Y;Zz)^H}OFn-4i{u0aswOQ{K)9WW`jw5}u9>hkvZkmArmZr6o5< z)I6yt$!OqqU352;7q|4Dc%SPBuR%TbkY~OV9gsO+#t{_Bw4n2oY}D@Gl%#FUVBzB> zULA>hlI(P8$M)ogY<*VsMFTPr5i+lwoJFTPOOX)Dv4?Zxz z@LQX+*2IrGX-(VQbvNRm_^N7{)sJWgWsxI@T2MvoMEwMPR9$BRY{#p`DX|9Hs3(q~ zs{yALvt$SMxsndgA(3O7`aq_@pI_+(Lb3gqH~!m@r$8k#vjVN~?Y8hqH6{0uDctjz zIxA;Y0_d8?tf8A{zo_TYKI=8spDU+O%;-hBU1?&E9EdA1z1whpA%xfi!MRVL z8b@`bN`7DUa-~BmUM*6h^25KineiTaY*dSch1xtYzQbN}tUx(-Z7=g4-?{IxRh`2@ zRA!hsvnd;99dCCSJA3jxc!Q_w%0!>jPx!$3k6rEg;P6s*!ngg^HZP^G=_@lQ_D=-8 zx7u@FHEhue%HV>}=wR5*tM!H>AHOZ`@W!@QoiV7~?rA|kR?1x3?xGWBl^)z#>h}Kk zvJ&zI<^SbOE3Yw3eIQ?^9P82*4-jqKHsImxNfkoVup1Ig6+Nr=+27n=#^Z3DdMBE` zPhp{yD}747;cjzZzro+Bd`Cagp2KRaT8=^Ta!8tG)bf{gf$nj0us^xn- zd(vakZ6wOu{nm77JENZHTWz_1HnTrCfyW|uQc)aRWaq56VUf3$Z&XHPiIq(u5}C%OFRm7=xI@SO7oz6CvF z-UxZZ25%HkR3zVSi6PUVBO!acV~*YFgUsx4e;w*H^h(eeTG1WmxqNXXFXSX*rcc&` zlf2sQ;GN`)DbQ*^Y>OT_0o#!;<3z}UeZgT)JKW`t?X}!3TBsf8^4Vd{NqpkYZ@dME zKGKIavZsf-F`*)NLFul$N_xZd&p1IIIFp7TLZ=;>bG7|=C3l78(^ zY)z$)lEjt zBYNqsat^erue}TJwiI0H0WWVXIyN7Adi~G8FyHsQjs2b^cACYX=!eWqbwRVxF7N^x zqVwkF-OlqoS6~(V+sEV}J_mxczJp~tlbDw0d@UzeWJo69OoDo5cj{kjYVolCaGSn+HuLoQU-I^bIiGl;2po+uUn8d^5wh7-8FXrS~7xFJe+q0yvmY?twz1~E6()Z2v_v+S+D<#YP zWV_oco^^w;w(=x*`(WJG{oC(wB1pC)>d9fAbQ7gyGkOi~%E;k~X421yMn69}h3jodUO$swek~{}$=aEMw1kex_%c)EDRJHVfsqSR`iX@XCA5*u;q^XJF*C z{_bQ;`gm;SN!}1CEgxH*y5W0Niv#jKe&v>E8m(OE&6XayB6Ej38=~9RJ+8)$Ssz#(ZNn$L;ESu(^8Aj%1qMvHnV&#%_MCG|1fE zM^8nYFgrX)J&rjJ`@pyJN`H6U8qTx>v>M~?9C;?@ zcqZr484y{)YgGjN>5HHI(COr`2CVBWxHQ(=%ufV6Uq#lAh>8C-$mg90gS}`I{~bZZ zaWC>Mzij3KnWCMX6G(cOqn(Ow(wRiOHn4zKL(v8&SOT zC92=Q_Uum5cwm^Bz4to&|6ZqG7i$)5VnfSpU}I}qpMS|&+m1OKv(J$h{IAG>3SR#XDrTLa=?`&9Dw=%3onq-yQHRZH|7hhHc_w(;kAn!P8{-$nIX^ z2&r)=*tYd}a$d1$y1zYp&Oe7^iRUxc6EJiO@&DxaIA(Rkst-S*20Mh~m?y_3_`1hG zTInpaE_wqyK_l46{m)r7tYb6eU2E|U(P+7qSe+7u5^1MmE`Ga6g(rR|tfz1rSN?P^ zyOKAeNp=+@#;a9%dbXf1$=?yXAdPr*Pu)%Ygfj%ikt zdJc{%j2Y)=9(8dLr!f&K{3P&~-Ne7ke3HKz@m)k(=AkN39j@W~%k3K%vA)JtuIJn` zb6!HND@MCm4bo6G1$zU_3F`wmPPxehJ26tdDvGjsf%HD7JN#q4uCpt_n0g$_g}m35 z8nB$;A#m^7_l$jt(INL&?0W896>;LLlsVN|?s-giSoAKR1-@tWL+s$P@+?ew)4fy! z`gT1#>c_hGy56Hh`{1bRfsf8ev6=eWV(-MRT#WqI`S-8ERGI1)O$i~YDpW;rJ>W#> z`DGrqJ?jOm#VqxJ;xZx{x{sLj*{2@TUiG9Yqe-QTi}jS^vhT4NfvG-JZVJA#Qx^{0 za$ujq3ur1bQGUd}Lodat*I}pJC?oM6%Zw6Q8;zKk|5bK9g>tW@_iz!vI>SPXovvzB zG*0;@q+OS4W(|?H>4-S<>Wu>MV_|Ke^MLr@~XPipFC`$eKVx_9_%%`%8hCuPOCF} b>CwuniK9MzN)8o4)$gh_b*?-tOU3Iq5SuQL diff --git a/README.md b/README.md index 8a5decb..a3d18f0 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,79 @@ -# 🧰 plain-git +# 🚀 plain-git +### _Operate Git in plain English — a fully interactive, human-friendly Git CLI_ -A lightweight, cross-platform Node.js + TypeScript library and CLI that lets developers perform Git actions through plain English or short commands — powered by the native `git` CLI. +![banner](https://dummyimage.com/1200x200/222/fff&text=plain-git) +*(Replace with your own banner if needed)* -## 🚀 Features -- Zero runtime dependencies -- Cross-platform (Windows, macOS, Linux) -- Can be used as CLI or programmatically -- Lightweight and developer-friendly +--- + +## 📦 What is plain-git? + +**plain-git** is a modern, interactive Git CLI that lets developers perform Git operations using **plain English**, guided prompts, clean menus, and a manager-driven architecture. + +Think of it as: + +> **Git for humans.** +> **A command palette inside your terminal.** +> **Zero memorization + maximum productivity.** + +--- + +## ✨ Features + +### 🖥 Beautiful interactive CLI +- ASCII banner +- Color-coded logs +- Scrollable menus +- Current branch indicator +- Clear grouped categories + +### 🧠 Full Manager-Based Architecture +Every domain of Git is isolated into a clean “Manager”: + +| Manager | Responsibilities | +|--------|------------------| +| **RepositoryManager** | init, clone, status, config, remotes, fsck, GC | +| **BranchManager** | create, rename, switch, delete, push upstream | +| **CommitManager** | stage, unstage, commit, amend, logs, diffs | +| **RemoteManager** | remotes, push, pull, fetch, sync | +| **StashManager** | stash create/apply/pop/drop/clear | +| **TagManager** | tags (LS, create, annotate, delete, push) | +| **MergeManager** | merge, conflicts, abort/continue | +| **ConflictManager** | conflict markers, editor open, diff | +| **RebaseManager** | rebase, interactive, skip/continue/abort | +| **HistoryManager** | history, reflog, diff, blame | +| **ResetManager** | soft/mixed/hard reset, discard changes | + +Everything runs interactively — no need to remember Git syntax. + +--- + +## 🧪 Full Test Suite (Vitest) + +plain-git ships with complete tests for: + +- RepositoryManager +- BranchManager +- CommitManager +- RemoteManager +- StashManager +- TagManager +- MergeManager +- ConflictManager +- RebaseManager +- HistoryManager +- ResetManager + +With: + +- Mocks for `inquirer`, `execSync`, `fs` +- Fully isolated sandbox environment +- Deterministic CI behavior +- Zero real Git side effects + +--- + +## 🔧 Installation -## 🧑‍💻 Usage ```bash -npx plain-git +npm install -g plain-git diff --git a/package.json b/package.json index 790a587..582236d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "plain-git", - "version": "1.0.0", + "version": "1.0.0-beta.1", "description": "A lightweight cross-platform CLI & library to perform Git operations using plain language commands — no dependencies, pure Node.js.", "main": "dist/index.js", "type": "commonjs", From 7851d2c9eb6714f1b4c5b5acc2c03d644ec65a5f Mon Sep 17 00:00:00 2001 From: Aditya Prakash <61825990+prakashaditya13@users.noreply.github.com> Date: Sat, 22 Nov 2025 01:50:21 +0530 Subject: [PATCH 3/7] Update README.md --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index a3d18f0..bcdf4d6 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,6 @@ ### _Operate Git in plain English — a fully interactive, human-friendly Git CLI_ ![banner](https://dummyimage.com/1200x200/222/fff&text=plain-git) -*(Replace with your own banner if needed)* --- From baa7072576c56c81a6050f5f2b2e56434e874552 Mon Sep 17 00:00:00 2001 From: prakashaditya13 Date: Sat, 22 Nov 2025 02:04:45 +0530 Subject: [PATCH 4/7] Upd: Package.json file for Prod build --- package.json | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 582236d..1d15e28 100644 --- a/package.json +++ b/package.json @@ -24,12 +24,23 @@ "developer-tools" ], "files": [ - "dist" + "dist/**/*", + "README.md", + "CHANGELOG.md", + "LICENSE" ], "engines": { "node": ">=16" }, "author": "Aditya Prakash", + "homepage": "https://github.com/prakashaditya13/plain-git#readme", + "repository": { + "type": "git", + "url": "git+https://github.com/prakashaditya13/plain-git.git" + }, + "bugs": { + "url": "https://github.com/prakashaditya13/plain-git/issues" + }, "license": "MIT", "devDependencies": { "@types/jest": "^30.0.0", From 00e45978444331fab6f9c6175cbdb58cf48a4756 Mon Sep 17 00:00:00 2001 From: prakashaditya13 Date: Sat, 22 Nov 2025 02:15:56 +0530 Subject: [PATCH 5/7] fix: beta prod config --- package.json | 3 +-- tsconfig.json | 1 + 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 1d15e28..06f25cf 100644 --- a/package.json +++ b/package.json @@ -3,11 +3,10 @@ "version": "1.0.0-beta.1", "description": "A lightweight cross-platform CLI & library to perform Git operations using plain language commands — no dependencies, pure Node.js.", "main": "dist/index.js", - "type": "commonjs", "scripts": { "start": "ts-node src/cli/index.ts", "dev": "tsx -r tsconfig-paths/register src/cli/index.ts", - "build": "tsc", + "build": "rimraf dist && tsc", "clean": "rimraf dist", "cli": "tsx src/cli/index.ts", "test": "vitest" diff --git a/tsconfig.json b/tsconfig.json index 6be23b7..cdf2f53 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,6 +3,7 @@ "target": "ES2022", "module": "commonjs", "moduleResolution": "node", + "allowJs": true, "outDir": "dist", "rootDir": "src", "strict": true, From 62464a97452047141768b38b442f43ffaa3c4bb5 Mon Sep 17 00:00:00 2001 From: prakashaditya13 Date: Sat, 22 Nov 2025 04:37:30 +0530 Subject: [PATCH 6/7] fix: auto remote + upstream for push action --- .gitignore | 3 ++- src/managers/RemoteManager.ts | 50 ++++++++++++++++++++++++++--------- 2 files changed, 39 insertions(+), 14 deletions(-) diff --git a/.gitignore b/.gitignore index 6f66877..36d161c 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ node_modules dist .DS_Store .env -coverage \ No newline at end of file +coverage +tests/sandbox \ No newline at end of file diff --git a/src/managers/RemoteManager.ts b/src/managers/RemoteManager.ts index 3e690c8..844d833 100644 --- a/src/managers/RemoteManager.ts +++ b/src/managers/RemoteManager.ts @@ -10,10 +10,9 @@ import inquirer from 'inquirer'; import { execSync } from 'child_process'; -import { GitExecutor } from '../core/GitExecutor'; +import { GitExecutor } from '../core/GitExecutor'; import { Logger } from '../utils/Logger'; - /** * Helper: get list of remote names */ @@ -122,18 +121,43 @@ export const RemoteManager = { */ async pushChanges() { const remotes = getRemoteList(); - const { remote } = await inquirer.prompt([ - { - type: 'list', - name: 'remote', - message: 'Select remote to push to:', - choices: remotes.length ? remotes : ['origin'], - }, - ]); + // Step 1 — If no remotes, ask user to add one + if (remotes.length === 0) { + Logger.error('❌ No remote found for this repository.'); + + const { url } = await inquirer.prompt([ + { type: 'input', name: 'url', message: "Enter remote URL to add as 'origin':" }, + ]); + + Logger.info(`🔗 Adding remote origin → ${url}`); + await GitExecutor.run(`git remote add origin ${url}`); + remotes.push('origin'); + } + + // Step 2 — Detect current branch + const currentBranch = execSync('git rev-parse --abbrev-ref HEAD', { encoding: 'utf-8' }).trim(); + + // Step 3 — Check if branch has upstream + let hasUpstream = true; + try { + execSync('git rev-parse --abbrev-ref --symbolic-full-name @{u}'); + } catch { + hasUpstream = false; + } + + // Step 4 — If no upstream, push -u + if (!hasUpstream) { + Logger.info(`🚀 First-time push detected for branch '${currentBranch}'.`); + Logger.info(`Setting upstream to origin/${currentBranch}...`); + await GitExecutor.run(`git push -u origin ${currentBranch}`); + Logger.success('✅ Pushed & upstream tracking set!'); + return; + } - Logger.info(`🚀 Pushing changes to '${remote}'...`); - await GitExecutor.run(`git push ${remote}`); - Logger.success(`✅ Changes pushed to '${remote}'!`); + // Step 5 — Normal push + Logger.info(`🚀 Pushing changes to remote...`); + await GitExecutor.run(`git push`); + Logger.success('✅ Changes pushed!'); }, /** From 28e78892abf70e7b5b1129a295f00cac0b234acd Mon Sep 17 00:00:00 2001 From: prakashaditya13 Date: Sat, 22 Nov 2025 05:02:20 +0530 Subject: [PATCH 7/7] test: RemoteManager --- tests/mocks/inquirerMock.ts | 4 + tests/unit/RemoteManager.test.ts | 259 +++++++++++++++++++------------ 2 files changed, 166 insertions(+), 97 deletions(-) diff --git a/tests/mocks/inquirerMock.ts b/tests/mocks/inquirerMock.ts index ccfdedb..a8aa85b 100644 --- a/tests/mocks/inquirerMock.ts +++ b/tests/mocks/inquirerMock.ts @@ -49,3 +49,7 @@ export function mockInquirer(answers: Record) { // ignore - tests will fail later if mock not present } } + +export function clearInquirerQueue() { + _answersQueue.length = 0; +} \ No newline at end of file diff --git a/tests/unit/RemoteManager.test.ts b/tests/unit/RemoteManager.test.ts index 1f2adf9..495fb51 100644 --- a/tests/unit/RemoteManager.test.ts +++ b/tests/unit/RemoteManager.test.ts @@ -1,6 +1,6 @@ -import { describe, it, expect, beforeEach, vi } from "vitest"; -import { GitExecutor } from "@/core/GitExecutor"; -import { mockInquirer } from "../mocks/inquirerMock"; +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { GitExecutor } from '@/core/GitExecutor'; +import { mockInquirer, clearInquirerQueue } from '../mocks/inquirerMock'; // Mock execSync BEFORE importing RemoteManager vi.mock('child_process', async (importOriginal) => { @@ -13,67 +13,66 @@ vi.mock('child_process', async (importOriginal) => { } as any; }); -import { execSync } from "child_process"; +import { execSync } from 'child_process'; -describe("RemoteManager – Full Test Suite", () => { +describe('RemoteManager – Full Test Suite', () => { beforeEach(() => { vi.clearAllMocks(); + clearInquirerQueue(); }); // -------------------------------------------------------------- // 1️⃣ listRemotes() // -------------------------------------------------------------- - it("should list remotes", async () => { - const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); + it('should list remotes', async () => { + const spy = vi.spyOn(GitExecutor, 'run').mockResolvedValue(); - const { RemoteManager } = await import("@/managers"); + const { RemoteManager } = await import('@/managers'); await RemoteManager.listRemotes(); - expect(spy).toHaveBeenCalledWith("git remote -v"); + expect(spy).toHaveBeenCalledWith('git remote -v'); }); // -------------------------------------------------------------- // 2️⃣ addRemote() // -------------------------------------------------------------- - it("should add a remote", async () => { - mockInquirer({ name: "origin", url: "https://github.com/test/repo" }); + it('should add a remote', async () => { + mockInquirer({ name: 'origin', url: 'https://github.com/test/repo' }); - const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); + const spy = vi.spyOn(GitExecutor, 'run').mockResolvedValue(); - const { RemoteManager } = await import("@/managers"); + const { RemoteManager } = await import('@/managers'); await RemoteManager.addRemote(); - expect(spy).toHaveBeenCalledWith( - "git remote add origin https://github.com/test/repo" - ); + expect(spy).toHaveBeenCalledWith('git remote add origin https://github.com/test/repo'); }); // -------------------------------------------------------------- // 3️⃣ renameRemote() // -------------------------------------------------------------- - it("should rename a remote", async () => { - (execSync as any).mockReturnValue("origin\nbackup"); + it('should rename a remote', async () => { + (execSync as any).mockReturnValue('origin\nbackup'); - mockInquirer({ oldName: "origin" }); - mockInquirer({ newName: "main-origin" }); + mockInquirer({ oldName: 'origin' }); + mockInquirer({ newName: 'main-origin' }); - const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); + const spy = vi.spyOn(GitExecutor, 'run').mockResolvedValue(); - const { RemoteManager } = await import("@/managers"); + const { RemoteManager } = await import('@/managers'); await RemoteManager.renameRemote(); - expect(spy).toHaveBeenCalledWith("git remote rename origin main-origin"); + expect(spy).toHaveBeenCalledWith('git remote rename origin main-origin'); }); - it("should not rename if no remotes exist", async () => { - (execSync as any).mockReturnValue(""); + it('should not rename if no remotes exist', async () => { + (execSync as any).mockReturnValue(''); - const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); + const spy = vi.spyOn(GitExecutor, 'run').mockResolvedValue(); - const { RemoteManager } = await import("@/managers"); + const { RemoteManager } = await import('@/managers'); await RemoteManager.renameRemote(); @@ -83,26 +82,26 @@ describe("RemoteManager – Full Test Suite", () => { // -------------------------------------------------------------- // 4️⃣ removeRemote() // -------------------------------------------------------------- - it("should remove remote", async () => { - (execSync as any).mockReturnValue("origin\nbackup"); + it('should remove remote', async () => { + (execSync as any).mockReturnValue('origin\nbackup'); - mockInquirer({ name: "backup" }); + mockInquirer({ name: 'backup' }); - const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); + const spy = vi.spyOn(GitExecutor, 'run').mockResolvedValue(); - const { RemoteManager } = await import("@/managers"); + const { RemoteManager } = await import('@/managers'); await RemoteManager.removeRemote(); - expect(spy).toHaveBeenCalledWith("git remote remove backup"); + expect(spy).toHaveBeenCalledWith('git remote remove backup'); }); - it("should not remove if no remotes", async () => { - (execSync as any).mockReturnValue(""); + it('should not remove if no remotes', async () => { + (execSync as any).mockReturnValue(''); - const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); + const spy = vi.spyOn(GitExecutor, 'run').mockResolvedValue(); - const { RemoteManager } = await import("@/managers"); + const { RemoteManager } = await import('@/managers'); await RemoteManager.removeRemote(); @@ -112,138 +111,204 @@ describe("RemoteManager – Full Test Suite", () => { // -------------------------------------------------------------- // 5️⃣ updateRemoteUrl() // -------------------------------------------------------------- - it("should update remote URL", async () => { - (execSync as any).mockReturnValue("origin"); + it('should update remote URL', async () => { + (execSync as any).mockReturnValue('origin'); - mockInquirer({ name: "origin" }); - mockInquirer({ url: "https://github.com/new/url" }); + mockInquirer({ name: 'origin' }); + mockInquirer({ url: 'https://github.com/new/url' }); - const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); + const spy = vi.spyOn(GitExecutor, 'run').mockResolvedValue(); - const { RemoteManager } = await import("@/managers"); + const { RemoteManager } = await import('@/managers'); await RemoteManager.updateRemoteUrl(); - expect(spy).toHaveBeenCalledWith( - "git remote set-url origin https://github.com/new/url" - ); + expect(spy).toHaveBeenCalledWith('git remote set-url origin https://github.com/new/url'); }); // -------------------------------------------------------------- // 6️⃣ pushChanges() // -------------------------------------------------------------- - it("should push changes to selected remote", async () => { - (execSync as any).mockReturnValue("origin\nbackup"); + it('should ask for remote URL and perform first push when NO remotes exist', async () => { + // No remotes in repo + (execSync as any).mockReturnValueOnce(''); // getRemoteList() + + // User enters remote URL + mockInquirer({ url: 'https://github.com/test/repo.git' }); - mockInquirer({ remote: "backup" }); + const executorSpy = vi.spyOn(GitExecutor, 'run').mockResolvedValue(); - const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); + const { RemoteManager } = await import('@/managers'); - const { RemoteManager } = await import("@/managers"); + // Mock: detecting currentBranch + (execSync as any).mockReturnValueOnce('main'); // current branch + (execSync as any).mockImplementationOnce(() => { + throw new Error(); + }); // no upstream await RemoteManager.pushChanges(); - expect(spy).toHaveBeenCalledWith("git push backup"); + expect(executorSpy).toHaveBeenCalledWith( + 'git remote add origin https://github.com/test/repo.git', + ); + + expect(executorSpy).toHaveBeenCalledWith('git push -u origin main'); }); - // If no remotes → default to origin - it("should push to origin when no remotes exist", async () => { - (execSync as any).mockReturnValue(""); + it('should push with -u when upstream is missing', async () => { + // Mock remotes exist + (execSync as any).mockReturnValueOnce('origin\n'); + + // No inquirer needed (remote auto-chooses origin) + mockInquirer({}); + + const executorSpy = vi.spyOn(GitExecutor, 'run').mockResolvedValue(); + + const { RemoteManager } = await import('@/managers'); + + // Mock branch name + (execSync as any).mockReturnValueOnce('dev'); // current branch + + // Mock: upstream missing -> execSync throws + (execSync as any).mockImplementationOnce(() => { + throw new Error('No upstream'); + }); + + await RemoteManager.pushChanges(); - mockInquirer({ remote: "origin" }); + expect(executorSpy).toHaveBeenCalledWith('git push -u origin dev'); + }); + + it('should perform normal push when upstream exists', async () => { + // Remote present + (execSync as any).mockReturnValueOnce('origin\n'); + + // No prompts necessary + mockInquirer({}); + + const executorSpy = vi.spyOn(GitExecutor, 'run').mockResolvedValue(); + + const { RemoteManager } = await import('@/managers'); + + // Mock branch = main + (execSync as any).mockReturnValueOnce('main'); + + // Mock upstream exists (does NOT throw) + (execSync as any).mockReturnValueOnce('origin/main'); + + await RemoteManager.pushChanges(); + + expect(executorSpy).toHaveBeenCalledWith('git push'); + }); - const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); - const { RemoteManager } = await import("@/managers"); + // If no remotes → ask for URL, add remote, then push + it('should push to origin when no remotes exist', async () => { + mockInquirer({ url: 'https://github.com/test/repo.git' }); + + // Mock execSync calls in order: + // 1. getRemoteList() -> execSync('git remote') -> returns '' + // 2. pushChanges() -> execSync('git rev-parse --abbrev-ref HEAD') -> returns 'main' + // 3. pushChanges() -> execSync('git rev-parse --abbrev-ref --symbolic-full-name @{u}') -> throws + (execSync as any) + .mockReturnValueOnce('') // getRemoteList() - no remotes + .mockReturnValueOnce('main') // current branch + .mockImplementationOnce(() => { + throw new Error(); // no upstream + }); + + const spy = vi.spyOn(GitExecutor, 'run').mockResolvedValue(); + const { RemoteManager } = await import('@/managers'); await RemoteManager.pushChanges(); - expect(spy).toHaveBeenCalledWith("git push origin"); + expect(spy).toHaveBeenCalledWith('git remote add origin https://github.com/test/repo.git'); + expect(spy).toHaveBeenCalledWith('git push -u origin main'); }); // -------------------------------------------------------------- // 7️⃣ pushWithUpstream() // -------------------------------------------------------------- - it("should push with upstream", async () => { - (execSync as any).mockReturnValue("origin"); + it('should push with upstream', async () => { + (execSync as any).mockReturnValue('origin'); - mockInquirer({ remote: "origin" }); - mockInquirer({ branch: "dev" }); + // pushWithUpstream uses a single prompt with array of 2 questions + mockInquirer({ remote: 'origin', branch: 'dev' }); - const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); + const spy = vi.spyOn(GitExecutor, 'run').mockResolvedValue(); - const { RemoteManager } = await import("@/managers"); + const { RemoteManager } = await import('@/managers'); await RemoteManager.pushWithUpstream(); - expect(spy).toHaveBeenCalledWith("git push -u origin dev"); + expect(spy).toHaveBeenCalledWith('git push -u origin dev'); }); // -------------------------------------------------------------- // 8️⃣ pullChanges() // -------------------------------------------------------------- - it("should pull from selected remote", async () => { - (execSync as any).mockReturnValue("origin\nteam"); + it('should pull from selected remote', async () => { + (execSync as any).mockReturnValue('origin\nteam'); - mockInquirer({ remote: "team" }); + mockInquirer({ remote: 'team' }); - const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); - const { RemoteManager } = await import("@/managers"); + const spy = vi.spyOn(GitExecutor, 'run').mockResolvedValue(); + const { RemoteManager } = await import('@/managers'); await RemoteManager.pullChanges(); - expect(spy).toHaveBeenCalledWith("git pull team"); + expect(spy).toHaveBeenCalledWith('git pull team'); }); // -------------------------------------------------------------- // 9️⃣ fetchUpdates() // -------------------------------------------------------------- - it("should fetch from selected remote", async () => { - (execSync as any).mockReturnValue("origin\ntest"); + it('should fetch from selected remote', async () => { + (execSync as any).mockReturnValue('origin\ntest'); - mockInquirer({ remote: "test" }); + mockInquirer({ remote: 'test' }); - const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); - const { RemoteManager } = await import("@/managers"); + const spy = vi.spyOn(GitExecutor, 'run').mockResolvedValue(); + const { RemoteManager } = await import('@/managers'); await RemoteManager.fetchUpdates(); - expect(spy).toHaveBeenCalledWith("git fetch test"); + expect(spy).toHaveBeenCalledWith('git fetch test'); }); it("should fetch --all when user selects 'all'", async () => { - (execSync as any).mockReturnValue("origin\nteam"); + (execSync as any).mockReturnValue('origin\nteam'); - mockInquirer({ remote: "all" }); + mockInquirer({ remote: 'all' }); - const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); - const { RemoteManager } = await import("@/managers"); + const spy = vi.spyOn(GitExecutor, 'run').mockResolvedValue(); + const { RemoteManager } = await import('@/managers'); await RemoteManager.fetchUpdates(); - expect(spy).toHaveBeenCalledWith("git fetch --all"); + expect(spy).toHaveBeenCalledWith('git fetch --all'); }); // -------------------------------------------------------------- // 🔟 showRemoteInfo() // -------------------------------------------------------------- - it("should show remote info", async () => { - (execSync as any).mockReturnValue("origin\nbackup"); + it('should show remote info', async () => { + (execSync as any).mockReturnValue('origin\nbackup'); - mockInquirer({ remote: "backup" }); + mockInquirer({ remote: 'backup' }); - const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); - const { RemoteManager } = await import("@/managers"); + const spy = vi.spyOn(GitExecutor, 'run').mockResolvedValue(); + const { RemoteManager } = await import('@/managers'); await RemoteManager.showRemoteInfo(); - expect(spy).toHaveBeenCalledWith("git remote show backup"); + expect(spy).toHaveBeenCalledWith('git remote show backup'); }); - it("should do nothing if no remotes exist", async () => { - (execSync as any).mockReturnValue(""); + it('should do nothing if no remotes exist', async () => { + (execSync as any).mockReturnValue(''); - const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); - const { RemoteManager } = await import("@/managers"); + const spy = vi.spyOn(GitExecutor, 'run').mockResolvedValue(); + const { RemoteManager } = await import('@/managers'); await RemoteManager.showRemoteInfo(); @@ -253,13 +318,13 @@ describe("RemoteManager – Full Test Suite", () => { // -------------------------------------------------------------- // 1️⃣1️⃣ syncAll() // -------------------------------------------------------------- - it("should sync all remotes", async () => { - const spy = vi.spyOn(GitExecutor, "run").mockResolvedValue(); + it('should sync all remotes', async () => { + const spy = vi.spyOn(GitExecutor, 'run').mockResolvedValue(); - const { RemoteManager } = await import("@/managers"); + const { RemoteManager } = await import('@/managers'); await RemoteManager.syncAll(); - expect(spy).toHaveBeenCalledWith("git fetch --all && git pull --all"); + expect(spy).toHaveBeenCalledWith('git fetch --all && git pull --all'); }); });