diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 9a745f3c..44b81dc3 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -40,7 +40,8 @@
"globals": "^17.6.0",
"typescript": "^6.0.3",
"typescript-eslint": "^8.59.4",
- "vite": "8.0.13"
+ "vite": "8.0.13",
+ "vitest": "^4.1.7"
},
"engines": {
"node": ">=22.0.0",
@@ -2639,6 +2640,17 @@
"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": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz",
@@ -2702,6 +2714,13 @@
"integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==",
"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": {
"version": "4.3.1",
"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": {
"version": "8.16.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
@@ -3192,6 +3324,16 @@
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
"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": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
@@ -3414,6 +3556,16 @@
],
"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": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz",
@@ -3856,6 +4008,13 @@
"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": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz",
@@ -4078,6 +4237,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",
@@ -4094,6 +4263,16 @@
"integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==",
"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": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@@ -5171,6 +5350,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/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@@ -5328,6 +5517,17 @@
"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": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/openapi-path-templating/-/openapi-path-templating-2.2.1.tgz",
@@ -5459,6 +5659,13 @@
"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": {
"version": "1.5.5",
"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"
}
},
+ "node_modules/siginfo": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
+ "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
+ "dev": true,
+ "license": "ISC"
+ },
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
@@ -6247,6 +6461,20 @@
"integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
"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": {
"version": "0.2.1",
"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==",
"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": {
"version": "0.2.16",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz",
@@ -6372,6 +6617,16 @@
"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": {
"version": "1.2.2",
"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": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz",
@@ -6766,6 +7111,23 @@
"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": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
diff --git a/frontend/package.json b/frontend/package.json
index 1f5e4759..68cdc664 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -14,6 +14,8 @@
"preview": "vite preview",
"lint": "eslint src",
"typecheck": "tsc --noEmit",
+ "test": "vitest run",
+ "test:watch": "vitest",
"gen:api": "node --experimental-strip-types --disable-warning=ExperimentalWarning scripts/build-openapi.mjs",
"gen:zod": "cd .. && go run ./tools/openapigen"
},
@@ -50,7 +52,8 @@
"globals": "^17.6.0",
"typescript": "^6.0.3",
"typescript-eslint": "^8.59.4",
- "vite": "8.0.13"
+ "vite": "8.0.13",
+ "vitest": "^4.1.7"
},
"overrides": {
"react-copy-to-clipboard": "^5.1.1",
diff --git a/frontend/src/test/__snapshots__/protocols.test.ts.snap b/frontend/src/test/__snapshots__/protocols.test.ts.snap
new file mode 100644
index 00000000..6682fe48
--- /dev/null
+++ b/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": "hy2-client@example.test",
+ "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": "ss-client-1@example.test",
+ "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": "carol@example.test",
+ "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": "alice@example.test",
+ "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": "bob@example.test",
+ "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=",
+ },
+}
+`;
diff --git a/frontend/src/test/golden/fixtures/inbound/hysteria2-basic.json b/frontend/src/test/golden/fixtures/inbound/hysteria2-basic.json
new file mode 100644
index 00000000..1339df5d
--- /dev/null
+++ b/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": "hy2-client@example.test",
+ "limitIp": 0,
+ "totalGB": 0,
+ "expiryTime": 0,
+ "enable": true,
+ "tgId": 0,
+ "subId": "hy2-001",
+ "comment": "",
+ "reset": 0
+ }
+ ]
+ }
+}
diff --git a/frontend/src/test/golden/fixtures/inbound/shadowsocks-2022.json b/frontend/src/test/golden/fixtures/inbound/shadowsocks-2022.json
new file mode 100644
index 00000000..57766ac4
--- /dev/null
+++ b/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": "ss-client-1@example.test",
+ "limitIp": 0,
+ "totalGB": 0,
+ "expiryTime": 0,
+ "enable": true,
+ "tgId": 0,
+ "subId": "ssm001",
+ "comment": "multi-user shadowsocks 2022",
+ "reset": 0
+ }
+ ],
+ "ivCheck": false
+ }
+}
diff --git a/frontend/src/test/golden/fixtures/inbound/trojan-basic.json b/frontend/src/test/golden/fixtures/inbound/trojan-basic.json
new file mode 100644
index 00000000..96574a07
--- /dev/null
+++ b/frontend/src/test/golden/fixtures/inbound/trojan-basic.json
@@ -0,0 +1,20 @@
+{
+ "protocol": "trojan",
+ "settings": {
+ "clients": [
+ {
+ "password": "tr0jan-passw0rd-XyZ-123!",
+ "email": "carol@example.test",
+ "limitIp": 0,
+ "totalGB": 0,
+ "expiryTime": 0,
+ "enable": true,
+ "tgId": 0,
+ "subId": "trj001",
+ "comment": "",
+ "reset": 0
+ }
+ ],
+ "fallbacks": []
+ }
+}
diff --git a/frontend/src/test/golden/fixtures/inbound/vless-tcp-none.json b/frontend/src/test/golden/fixtures/inbound/vless-tcp-none.json
new file mode 100644
index 00000000..906d069e
--- /dev/null
+++ b/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": "alice@example.test",
+ "flow": "",
+ "limitIp": 0,
+ "totalGB": 0,
+ "expiryTime": 0,
+ "enable": true,
+ "tgId": 0,
+ "subId": "abc123def",
+ "comment": "",
+ "reset": 0
+ }
+ ],
+ "decryption": "none",
+ "encryption": "none",
+ "fallbacks": []
+ }
+}
diff --git a/frontend/src/test/golden/fixtures/inbound/vmess-basic.json b/frontend/src/test/golden/fixtures/inbound/vmess-basic.json
new file mode 100644
index 00000000..6cde4c5a
--- /dev/null
+++ b/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": "bob@example.test",
+ "limitIp": 2,
+ "totalGB": 0,
+ "expiryTime": 0,
+ "enable": true,
+ "tgId": 0,
+ "subId": "vmess001",
+ "comment": "primary tester",
+ "reset": 0
+ }
+ ]
+ }
+}
diff --git a/frontend/src/test/golden/fixtures/inbound/wireguard-basic.json b/frontend/src/test/golden/fixtures/inbound/wireguard-basic.json
new file mode 100644
index 00000000..72b64622
--- /dev/null
+++ b/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
+ }
+}
diff --git a/frontend/src/test/protocols.test.ts b/frontend/src/test/protocols.test.ts
new file mode 100644
index 00000000..45a8f6a5
--- /dev/null
+++ b/frontend/src/test/protocols.test.ts
@@ -0,0 +1,29 @@
+///
+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(
+ './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();
+ });
+ }
+});
diff --git a/frontend/vitest.config.ts b/frontend/vitest.config.ts
new file mode 100644
index 00000000..c81d8893
--- /dev/null
+++ b/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,
+ },
+});