Переглянути джерело

test(frontend): vitest harness with golden-file fixtures for inbound protocols

Stand up Phase 3 safety net before the models/ rewrite. The harness loads
JSON fixtures via Vite's import.meta.glob, parses each through
InboundSettingsSchema (the tagged-wrapper DU), and snapshots the canonical
parsed shape. Snapshots stay byte-stable across the upcoming class-to-
pure-function extraction, catching any normalization drift.

Six representative inbound fixtures cover the high-traffic protocols:
vless, vmess, trojan, shadowsocks (2022-blake3 multi-user), wireguard,
hysteria2. Stream and security branches plus the remaining protocols
(http, mixed, tunnel, hysteria) follow in subsequent turns.

Uses /// <reference types="vite/client" /> instead of @types/node so we
avoid pulling in another type package; import.meta.glob is enough to walk
the fixtures directory at compile time.

Adds vitest 4.1.7 as the only new dev dependency. test/test:watch scripts
land in package.json; a standalone vitest.config.ts keeps the production
vite.config.js (which reads from sqlite via DatabaseSync) out of the test
runner.
MHSanaei 23 годин тому
батько
коміт
a9359e921b

+ 363 - 1
frontend/package-lock.json

@@ -40,7 +40,8 @@
         "globals": "^17.6.0",
         "globals": "^17.6.0",
         "typescript": "^6.0.3",
         "typescript": "^6.0.3",
         "typescript-eslint": "^8.59.4",
         "typescript-eslint": "^8.59.4",
-        "vite": "8.0.13"
+        "vite": "8.0.13",
+        "vitest": "^4.1.7"
       },
       },
       "engines": {
       "engines": {
         "node": ">=22.0.0",
         "node": ">=22.0.0",
@@ -2639,6 +2640,17 @@
         "tslib": "^2.4.0"
         "tslib": "^2.4.0"
       }
       }
     },
     },
+    "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/d3-array": {
     "node_modules/@types/d3-array": {
       "version": "3.2.2",
       "version": "3.2.2",
       "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz",
       "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz",
@@ -2702,6 +2714,13 @@
       "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==",
       "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==",
       "license": "MIT"
       "license": "MIT"
     },
     },
+    "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/esrecurse": {
     "node_modules/@types/esrecurse": {
       "version": "4.3.1",
       "version": "4.3.1",
       "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz",
       "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz",
@@ -3065,6 +3084,119 @@
         }
         }
       }
       }
     },
     },
+    "node_modules/@vitest/expect": {
+      "version": "4.1.7",
+      "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.7.tgz",
+      "integrity": "sha512-1R+tw0ortHEbZDGMymm+pN7/AFQ/RkFFdtd7EN+VBpynKmLbP8A3rpEXdshBJ7+8hQ9zBJh/i1s0yKNtxAnU7w==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@standard-schema/spec": "^1.1.0",
+        "@types/chai": "^5.2.2",
+        "@vitest/spy": "4.1.7",
+        "@vitest/utils": "4.1.7",
+        "chai": "^6.2.2",
+        "tinyrainbow": "^3.1.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/vitest"
+      }
+    },
+    "node_modules/@vitest/mocker": {
+      "version": "4.1.7",
+      "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.7.tgz",
+      "integrity": "sha512-vY7nuamKgfvpA1Koa3oYIw/k7D6kZnpGyNMZW8loow2bsBYla1TFdqTaXncWdRn4pgwNs+90RhnXhJScDwQeJA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@vitest/spy": "4.1.7",
+        "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 || ^8.0.0"
+      },
+      "peerDependenciesMeta": {
+        "msw": {
+          "optional": true
+        },
+        "vite": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@vitest/pretty-format": {
+      "version": "4.1.7",
+      "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.7.tgz",
+      "integrity": "sha512-umgCarTOYQWIaDMvGDRZij+6b9oVeLIyJzfN+AS88e0ZOU3QTgNNSTtjQOpcvWr3np1N0j4WgZj+sb3oYBDscw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "tinyrainbow": "^3.1.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/vitest"
+      }
+    },
+    "node_modules/@vitest/runner": {
+      "version": "4.1.7",
+      "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.7.tgz",
+      "integrity": "sha512-BapjmAQ2aI78WdMEfeUWivnfVzB+VPGwWRQcJE0OUq7qEeEcBsCSf+0T5iREBNE5nBb4wA5Ya0W6IA+sghdEFw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@vitest/utils": "4.1.7",
+        "pathe": "^2.0.3"
+      },
+      "funding": {
+        "url": "https://opencollective.com/vitest"
+      }
+    },
+    "node_modules/@vitest/snapshot": {
+      "version": "4.1.7",
+      "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.7.tgz",
+      "integrity": "sha512-ZacLzja+TmJeZ1h14xW2FB/WpeimUD3haBXQPyJqxvo8jQTmfeA8zv58mtjN2C7EHXZDYVcVYdYmAxjkWVvKCw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@vitest/pretty-format": "4.1.7",
+        "@vitest/utils": "4.1.7",
+        "magic-string": "^0.30.21",
+        "pathe": "^2.0.3"
+      },
+      "funding": {
+        "url": "https://opencollective.com/vitest"
+      }
+    },
+    "node_modules/@vitest/spy": {
+      "version": "4.1.7",
+      "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.7.tgz",
+      "integrity": "sha512-kbkI5LMWakyuTIvs6fUJ5qdIVb1XVKsYJAT4OJ938cHMROYMSfmoQdZy0aaAnjbbc8F61vkoTqz/Az+/HiIu5Q==",
+      "dev": true,
+      "license": "MIT",
+      "funding": {
+        "url": "https://opencollective.com/vitest"
+      }
+    },
+    "node_modules/@vitest/utils": {
+      "version": "4.1.7",
+      "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.7.tgz",
+      "integrity": "sha512-T532WBu791cBxJlCl6SO+J14l81DQx6uQHm1bQbmCDY7nqlEIgkza/UFnSBNaUtSf41unldDFjdOBYEQC4b5Hw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@vitest/pretty-format": "4.1.7",
+        "convert-source-map": "^2.0.0",
+        "tinyrainbow": "^3.1.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/vitest"
+      }
+    },
     "node_modules/acorn": {
     "node_modules/acorn": {
       "version": "8.16.0",
       "version": "8.16.0",
       "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
       "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
@@ -3192,6 +3324,16 @@
       "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
       "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
       "license": "Python-2.0"
       "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/asynckit": {
     "node_modules/asynckit": {
       "version": "0.4.0",
       "version": "0.4.0",
       "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
       "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
@@ -3414,6 +3556,16 @@
       ],
       ],
       "license": "CC-BY-4.0"
       "license": "CC-BY-4.0"
     },
     },
+    "node_modules/chai": {
+      "version": "6.2.2",
+      "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz",
+      "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      }
+    },
     "node_modules/character-entities": {
     "node_modules/character-entities": {
       "version": "2.0.2",
       "version": "2.0.2",
       "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz",
       "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz",
@@ -3856,6 +4008,13 @@
         "node": ">= 0.4"
         "node": ">= 0.4"
       }
       }
     },
     },
+    "node_modules/es-module-lexer": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz",
+      "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==",
+      "dev": true,
+      "license": "MIT"
+    },
     "node_modules/es-object-atoms": {
     "node_modules/es-object-atoms": {
       "version": "1.1.2",
       "version": "1.1.2",
       "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz",
       "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz",
@@ -4078,6 +4237,16 @@
         "node": ">=4.0"
         "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": {
     "node_modules/esutils": {
       "version": "2.0.3",
       "version": "2.0.3",
       "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
       "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
@@ -4094,6 +4263,16 @@
       "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==",
       "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==",
       "license": "MIT"
       "license": "MIT"
     },
     },
+    "node_modules/expect-type": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz",
+      "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=12.0.0"
+      }
+    },
     "node_modules/fast-deep-equal": {
     "node_modules/fast-deep-equal": {
       "version": "3.1.3",
       "version": "3.1.3",
       "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
       "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@@ -5171,6 +5350,16 @@
         "yallist": "^3.0.2"
         "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/math-intrinsics": {
     "node_modules/math-intrinsics": {
       "version": "1.1.0",
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
       "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@@ -5328,6 +5517,17 @@
         "url": "https://github.com/sponsors/ljharb"
         "url": "https://github.com/sponsors/ljharb"
       }
       }
     },
     },
+    "node_modules/obug": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz",
+      "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==",
+      "dev": true,
+      "funding": [
+        "https://github.com/sponsors/sxzz",
+        "https://opencollective.com/debug"
+      ],
+      "license": "MIT"
+    },
     "node_modules/openapi-path-templating": {
     "node_modules/openapi-path-templating": {
       "version": "2.2.1",
       "version": "2.2.1",
       "resolved": "https://registry.npmjs.org/openapi-path-templating/-/openapi-path-templating-2.2.1.tgz",
       "resolved": "https://registry.npmjs.org/openapi-path-templating/-/openapi-path-templating-2.2.1.tgz",
@@ -5459,6 +5659,13 @@
         "node": ">=8"
         "node": ">=8"
       }
       }
     },
     },
+    "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/persian-calendar-suite": {
     "node_modules/persian-calendar-suite": {
       "version": "1.5.5",
       "version": "1.5.5",
       "resolved": "https://registry.npmjs.org/persian-calendar-suite/-/persian-calendar-suite-1.5.5.tgz",
       "resolved": "https://registry.npmjs.org/persian-calendar-suite/-/persian-calendar-suite-1.5.5.tgz",
@@ -6221,6 +6428,13 @@
         "url": "https://github.com/sponsors/ljharb"
         "url": "https://github.com/sponsors/ljharb"
       }
       }
     },
     },
+    "node_modules/siginfo": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
+      "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
+      "dev": true,
+      "license": "ISC"
+    },
     "node_modules/source-map-js": {
     "node_modules/source-map-js": {
       "version": "1.2.1",
       "version": "1.2.1",
       "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
       "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
@@ -6247,6 +6461,20 @@
       "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
       "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
       "license": "BSD-3-Clause"
       "license": "BSD-3-Clause"
     },
     },
+    "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": "4.1.0",
+      "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz",
+      "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==",
+      "dev": true,
+      "license": "MIT"
+    },
     "node_modules/string-convert": {
     "node_modules/string-convert": {
       "version": "0.2.1",
       "version": "0.2.1",
       "resolved": "https://registry.npmjs.org/string-convert/-/string-convert-0.2.1.tgz",
       "resolved": "https://registry.npmjs.org/string-convert/-/string-convert-0.2.1.tgz",
@@ -6355,6 +6583,23 @@
       "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==",
       "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==",
       "license": "MIT"
       "license": "MIT"
     },
     },
+    "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": "1.2.2",
+      "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.2.tgz",
+      "integrity": "sha512-M/Q0B2cp4K7kynaT/vnED1j8TlLY+Pp7C6Wl2bl/7u/F0mUVwdyOpwomQb8JpYLitHUssAJRmLZdMCGsrx7i+g==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      }
+    },
     "node_modules/tinyglobby": {
     "node_modules/tinyglobby": {
       "version": "0.2.16",
       "version": "0.2.16",
       "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz",
       "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz",
@@ -6372,6 +6617,16 @@
         "url": "https://github.com/sponsors/SuperchupuDev"
         "url": "https://github.com/sponsors/SuperchupuDev"
       }
       }
     },
     },
+    "node_modules/tinyrainbow": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz",
+      "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=14.0.0"
+      }
+    },
     "node_modules/to-buffer": {
     "node_modules/to-buffer": {
       "version": "1.2.2",
       "version": "1.2.2",
       "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.2.tgz",
       "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.2.tgz",
@@ -6707,6 +6962,96 @@
         }
         }
       }
       }
     },
     },
+    "node_modules/vitest": {
+      "version": "4.1.7",
+      "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.7.tgz",
+      "integrity": "sha512-flYyaFd2CgoCoU+0UKt3pxksgC+S02iTDN0n3LtqaMeXsI9SBcdNujc2k0DeFLzUn/0k538yNjOSdwgCqcrwJA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@vitest/expect": "4.1.7",
+        "@vitest/mocker": "4.1.7",
+        "@vitest/pretty-format": "4.1.7",
+        "@vitest/runner": "4.1.7",
+        "@vitest/snapshot": "4.1.7",
+        "@vitest/spy": "4.1.7",
+        "@vitest/utils": "4.1.7",
+        "es-module-lexer": "^2.0.0",
+        "expect-type": "^1.3.0",
+        "magic-string": "^0.30.21",
+        "obug": "^2.1.1",
+        "pathe": "^2.0.3",
+        "picomatch": "^4.0.3",
+        "std-env": "^4.0.0-rc.1",
+        "tinybench": "^2.9.0",
+        "tinyexec": "^1.0.2",
+        "tinyglobby": "^0.2.15",
+        "tinyrainbow": "^3.1.0",
+        "vite": "^6.0.0 || ^7.0.0 || ^8.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": "*",
+        "@opentelemetry/api": "^1.9.0",
+        "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0",
+        "@vitest/browser-playwright": "4.1.7",
+        "@vitest/browser-preview": "4.1.7",
+        "@vitest/browser-webdriverio": "4.1.7",
+        "@vitest/coverage-istanbul": "4.1.7",
+        "@vitest/coverage-v8": "4.1.7",
+        "@vitest/ui": "4.1.7",
+        "happy-dom": "*",
+        "jsdom": "*",
+        "vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
+      },
+      "peerDependenciesMeta": {
+        "@edge-runtime/vm": {
+          "optional": true
+        },
+        "@opentelemetry/api": {
+          "optional": true
+        },
+        "@types/node": {
+          "optional": true
+        },
+        "@vitest/browser-playwright": {
+          "optional": true
+        },
+        "@vitest/browser-preview": {
+          "optional": true
+        },
+        "@vitest/browser-webdriverio": {
+          "optional": true
+        },
+        "@vitest/coverage-istanbul": {
+          "optional": true
+        },
+        "@vitest/coverage-v8": {
+          "optional": true
+        },
+        "@vitest/ui": {
+          "optional": true
+        },
+        "happy-dom": {
+          "optional": true
+        },
+        "jsdom": {
+          "optional": true
+        },
+        "vite": {
+          "optional": false
+        }
+      }
+    },
     "node_modules/void-elements": {
     "node_modules/void-elements": {
       "version": "3.1.0",
       "version": "3.1.0",
       "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz",
       "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz",
@@ -6766,6 +7111,23 @@
         "url": "https://github.com/sponsors/ljharb"
         "url": "https://github.com/sponsors/ljharb"
       }
       }
     },
     },
+    "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": {
     "node_modules/word-wrap": {
       "version": "1.2.5",
       "version": "1.2.5",
       "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
       "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",

+ 4 - 1
frontend/package.json

@@ -14,6 +14,8 @@
     "preview": "vite preview",
     "preview": "vite preview",
     "lint": "eslint src",
     "lint": "eslint src",
     "typecheck": "tsc --noEmit",
     "typecheck": "tsc --noEmit",
+    "test": "vitest run",
+    "test:watch": "vitest",
     "gen:api": "node --experimental-strip-types --disable-warning=ExperimentalWarning scripts/build-openapi.mjs",
     "gen:api": "node --experimental-strip-types --disable-warning=ExperimentalWarning scripts/build-openapi.mjs",
     "gen:zod": "cd .. && go run ./tools/openapigen"
     "gen:zod": "cd .. && go run ./tools/openapigen"
   },
   },
@@ -50,7 +52,8 @@
     "globals": "^17.6.0",
     "globals": "^17.6.0",
     "typescript": "^6.0.3",
     "typescript": "^6.0.3",
     "typescript-eslint": "^8.59.4",
     "typescript-eslint": "^8.59.4",
-    "vite": "8.0.13"
+    "vite": "8.0.13",
+    "vitest": "^4.1.7"
   },
   },
   "overrides": {
   "overrides": {
     "react-copy-to-clipboard": "^5.1.1",
     "react-copy-to-clipboard": "^5.1.1",

+ 144 - 0
frontend/src/test/__snapshots__/protocols.test.ts.snap

@@ -0,0 +1,144 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`InboundSettingsSchema fixtures > parses hysteria2-basic byte-stably 1`] = `
+{
+  "protocol": "hysteria2",
+  "settings": {
+    "clients": [
+      {
+        "auth": "hyst3ria2-auth-token-XYZ",
+        "comment": "",
+        "email": "[email protected]",
+        "enable": true,
+        "expiryTime": 0,
+        "limitIp": 0,
+        "reset": 0,
+        "subId": "hy2-001",
+        "tgId": 0,
+        "totalGB": 0,
+      },
+    ],
+    "version": 2,
+  },
+}
+`;
+
+exports[`InboundSettingsSchema fixtures > parses shadowsocks-2022 byte-stably 1`] = `
+{
+  "protocol": "shadowsocks",
+  "settings": {
+    "clients": [
+      {
+        "comment": "multi-user shadowsocks 2022",
+        "email": "[email protected]",
+        "enable": true,
+        "expiryTime": 0,
+        "limitIp": 0,
+        "method": "",
+        "password": "dGVzdC1jbGllbnQtcGFzc3dvcmQtMQ==",
+        "reset": 0,
+        "subId": "ssm001",
+        "tgId": 0,
+        "totalGB": 0,
+      },
+    ],
+    "ivCheck": false,
+    "method": "2022-blake3-aes-256-gcm",
+    "network": "tcp,udp",
+    "password": "9oCBhTZxJ5wQa3fLs2vK7nM6pR4tY1uX",
+  },
+}
+`;
+
+exports[`InboundSettingsSchema fixtures > parses trojan-basic byte-stably 1`] = `
+{
+  "protocol": "trojan",
+  "settings": {
+    "clients": [
+      {
+        "comment": "",
+        "email": "[email protected]",
+        "enable": true,
+        "expiryTime": 0,
+        "limitIp": 0,
+        "password": "tr0jan-passw0rd-XyZ-123!",
+        "reset": 0,
+        "subId": "trj001",
+        "tgId": 0,
+        "totalGB": 0,
+      },
+    ],
+    "fallbacks": [],
+  },
+}
+`;
+
+exports[`InboundSettingsSchema fixtures > parses vless-tcp-none byte-stably 1`] = `
+{
+  "protocol": "vless",
+  "settings": {
+    "clients": [
+      {
+        "comment": "",
+        "email": "[email protected]",
+        "enable": true,
+        "expiryTime": 0,
+        "flow": "",
+        "id": "8c14d6f7-2e3b-4a91-9d24-3f7a6b8c1e02",
+        "limitIp": 0,
+        "reset": 0,
+        "subId": "abc123def",
+        "tgId": 0,
+        "totalGB": 0,
+      },
+    ],
+    "decryption": "none",
+    "encryption": "none",
+    "fallbacks": [],
+  },
+}
+`;
+
+exports[`InboundSettingsSchema fixtures > parses vmess-basic byte-stably 1`] = `
+{
+  "protocol": "vmess",
+  "settings": {
+    "clients": [
+      {
+        "comment": "primary tester",
+        "email": "[email protected]",
+        "enable": true,
+        "expiryTime": 0,
+        "id": "c0aa1b9e-4d56-4e8b-9a01-bf2e5d7c4f31",
+        "limitIp": 2,
+        "reset": 0,
+        "security": "auto",
+        "subId": "vmess001",
+        "tgId": 0,
+        "totalGB": 0,
+      },
+    ],
+  },
+}
+`;
+
+exports[`InboundSettingsSchema fixtures > parses wireguard-basic byte-stably 1`] = `
+{
+  "protocol": "wireguard",
+  "settings": {
+    "mtu": 1420,
+    "noKernelTun": false,
+    "peers": [
+      {
+        "allowedIPs": [
+          "10.0.0.2/32",
+        ],
+        "keepAlive": 25,
+        "privateKey": "iJ2cBkrSGqRwIfYIDIxk7hr5RXfdR93MfJUL7yqkkH8=",
+        "publicKey": "DGSYIcEKAUkA7HhzGSjxLZuV67BR3LeyU0BMLJzNVHQ=",
+      },
+    ],
+    "secretKey": "QGVlb2dXc1ZTWGw0ZXBzZndsWmtMaUM5MUlNYjBHWFdYbz0=",
+  },
+}
+`;

+ 20 - 0
frontend/src/test/golden/fixtures/inbound/hysteria2-basic.json

@@ -0,0 +1,20 @@
+{
+  "protocol": "hysteria2",
+  "settings": {
+    "version": 2,
+    "clients": [
+      {
+        "auth": "hyst3ria2-auth-token-XYZ",
+        "email": "[email protected]",
+        "limitIp": 0,
+        "totalGB": 0,
+        "expiryTime": 0,
+        "enable": true,
+        "tgId": 0,
+        "subId": "hy2-001",
+        "comment": "",
+        "reset": 0
+      }
+    ]
+  }
+}

+ 24 - 0
frontend/src/test/golden/fixtures/inbound/shadowsocks-2022.json

@@ -0,0 +1,24 @@
+{
+  "protocol": "shadowsocks",
+  "settings": {
+    "method": "2022-blake3-aes-256-gcm",
+    "password": "9oCBhTZxJ5wQa3fLs2vK7nM6pR4tY1uX",
+    "network": "tcp,udp",
+    "clients": [
+      {
+        "method": "",
+        "password": "dGVzdC1jbGllbnQtcGFzc3dvcmQtMQ==",
+        "email": "[email protected]",
+        "limitIp": 0,
+        "totalGB": 0,
+        "expiryTime": 0,
+        "enable": true,
+        "tgId": 0,
+        "subId": "ssm001",
+        "comment": "multi-user shadowsocks 2022",
+        "reset": 0
+      }
+    ],
+    "ivCheck": false
+  }
+}

+ 20 - 0
frontend/src/test/golden/fixtures/inbound/trojan-basic.json

@@ -0,0 +1,20 @@
+{
+  "protocol": "trojan",
+  "settings": {
+    "clients": [
+      {
+        "password": "tr0jan-passw0rd-XyZ-123!",
+        "email": "[email protected]",
+        "limitIp": 0,
+        "totalGB": 0,
+        "expiryTime": 0,
+        "enable": true,
+        "tgId": 0,
+        "subId": "trj001",
+        "comment": "",
+        "reset": 0
+      }
+    ],
+    "fallbacks": []
+  }
+}

+ 23 - 0
frontend/src/test/golden/fixtures/inbound/vless-tcp-none.json

@@ -0,0 +1,23 @@
+{
+  "protocol": "vless",
+  "settings": {
+    "clients": [
+      {
+        "id": "8c14d6f7-2e3b-4a91-9d24-3f7a6b8c1e02",
+        "email": "[email protected]",
+        "flow": "",
+        "limitIp": 0,
+        "totalGB": 0,
+        "expiryTime": 0,
+        "enable": true,
+        "tgId": 0,
+        "subId": "abc123def",
+        "comment": "",
+        "reset": 0
+      }
+    ],
+    "decryption": "none",
+    "encryption": "none",
+    "fallbacks": []
+  }
+}

+ 20 - 0
frontend/src/test/golden/fixtures/inbound/vmess-basic.json

@@ -0,0 +1,20 @@
+{
+  "protocol": "vmess",
+  "settings": {
+    "clients": [
+      {
+        "id": "c0aa1b9e-4d56-4e8b-9a01-bf2e5d7c4f31",
+        "security": "auto",
+        "email": "[email protected]",
+        "limitIp": 2,
+        "totalGB": 0,
+        "expiryTime": 0,
+        "enable": true,
+        "tgId": 0,
+        "subId": "vmess001",
+        "comment": "primary tester",
+        "reset": 0
+      }
+    ]
+  }
+}

+ 16 - 0
frontend/src/test/golden/fixtures/inbound/wireguard-basic.json

@@ -0,0 +1,16 @@
+{
+  "protocol": "wireguard",
+  "settings": {
+    "mtu": 1420,
+    "secretKey": "QGVlb2dXc1ZTWGw0ZXBzZndsWmtMaUM5MUlNYjBHWFdYbz0=",
+    "peers": [
+      {
+        "privateKey": "iJ2cBkrSGqRwIfYIDIxk7hr5RXfdR93MfJUL7yqkkH8=",
+        "publicKey": "DGSYIcEKAUkA7HhzGSjxLZuV67BR3LeyU0BMLJzNVHQ=",
+        "allowedIPs": ["10.0.0.2/32"],
+        "keepAlive": 25
+      }
+    ],
+    "noKernelTun": false
+  }
+}

+ 29 - 0
frontend/src/test/protocols.test.ts

@@ -0,0 +1,29 @@
+/// <reference types="vite/client" />
+import { describe, expect, it } from 'vitest';
+
+import { InboundSettingsSchema } from '@/schemas/protocols';
+
+// import.meta.glob (eager, default-import) gives us {path: parsedJson} at
+// compile time — no fs, no @types/node. Vitest inherits the vite/client
+// shape so this stays typed.
+const inboundFixtures = import.meta.glob<unknown>(
+  './golden/fixtures/inbound/*.json',
+  { eager: true, import: 'default' },
+);
+
+function fixtureName(path: string): string {
+  const file = path.split('/').pop() ?? path;
+  return file.replace(/\.json$/, '');
+}
+
+describe('InboundSettingsSchema fixtures', () => {
+  const entries = Object.entries(inboundFixtures).sort(([a], [b]) => a.localeCompare(b));
+  expect(entries.length, 'expected at least one fixture under golden/fixtures/inbound').toBeGreaterThan(0);
+
+  for (const [path, raw] of entries) {
+    it(`parses ${fixtureName(path)} byte-stably`, () => {
+      const parsed = InboundSettingsSchema.parse(raw);
+      expect(parsed).toMatchSnapshot();
+    });
+  }
+});

+ 16 - 0
frontend/vitest.config.ts

@@ -0,0 +1,16 @@
+import path from 'node:path';
+
+import { defineConfig } from 'vitest/config';
+
+export default defineConfig({
+  resolve: {
+    alias: {
+      '@': path.resolve(__dirname, 'src'),
+    },
+  },
+  test: {
+    include: ['src/test/**/*.test.ts'],
+    environment: 'node',
+    globals: false,
+  },
+});