Преглед изворни кода

test(frontend): convert legacy-class parity tests to snapshot baselines

With the inbound/outbound modal rewrites complete, the cross-check
against the legacy Inbound class has served its purpose. The new
pure-function / Zod-schema paths are the source of truth for production
code; the parity assertions were the migration safety net.

Convert the three parity test files to snapshot-based regression tests:

- headers.test.ts: toHeaders + toV2Headers run against snapshots
  captured at the close of the migration (when both new and legacy
  were verified byte-equal).
- protocol-capabilities.test.ts: 140 cases (10 fixtures × 14 stream
  shapes) snapshot the predicate-result tuple. Was: parity vs legacy
  Inbound.canEnableX() class methods.
- inbound-link.test.ts: per-protocol genXxxLink + genInboundLinks
  orchestrator output is snapshotted. Was: byte-equality vs legacy
  Inbound.genXxxLink() methods.

Also delete shadow.test.ts — its purpose was a dual-parse drift
detector (Inbound.Settings.fromJson vs InboundSettingsSchema.parse).
inbound-full.test.ts already snapshots the Zod parse output, which
covers the same ground without the legacy dependency.

models/inbound.ts and models/outbound.ts stay in the tree for now —
DBInbound still consumes Inbound via its toInbound() method, and
DBInbound migration is out of scope per the migration spec
('Do NOT migrate Status, DBInbound, or AllSetting...'). No
production page imports from @/models/inbound or @/models/outbound
directly anymore.
MHSanaei пре 9 часа
родитељ
комит
71631fd4dc

+ 118 - 0
frontend/src/test/__snapshots__/headers.test.ts.snap

@@ -0,0 +1,118 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`toHeaders > empty 1`] = `[]`;
+
+exports[`toHeaders > mixed 1`] = `
+[
+  {
+    "name": "Host",
+    "value": "a.example.test",
+  },
+  {
+    "name": "X-Trace",
+    "value": "1",
+  },
+  {
+    "name": "X-Trace",
+    "value": "2",
+  },
+]
+`;
+
+exports[`toHeaders > multi array 1`] = `
+[
+  {
+    "name": "Accept",
+    "value": "text/html",
+  },
+  {
+    "name": "Accept",
+    "value": "application/json",
+  },
+]
+`;
+
+exports[`toHeaders > null 1`] = `[]`;
+
+exports[`toHeaders > primitive 1`] = `[]`;
+
+exports[`toHeaders > single array 1`] = `
+[
+  {
+    "name": "Host",
+    "value": "a.example.test",
+  },
+]
+`;
+
+exports[`toHeaders > single string 1`] = `
+[
+  {
+    "name": "Host",
+    "value": "example.test",
+  },
+]
+`;
+
+exports[`toHeaders > undefined 1`] = `[]`;
+
+exports[`toV2Headers (arr=false) > duplicate name 1`] = `
+{
+  "Accept": "application/json",
+}
+`;
+
+exports[`toV2Headers (arr=false) > empty 1`] = `{}`;
+
+exports[`toV2Headers (arr=false) > empty name skipped 1`] = `
+{
+  "X-Real": "kept",
+}
+`;
+
+exports[`toV2Headers (arr=false) > empty value skipped 1`] = `
+{
+  "X-Real": "kept",
+}
+`;
+
+exports[`toV2Headers (arr=false) > single 1`] = `
+{
+  "Host": "example.test",
+}
+`;
+
+exports[`toV2Headers (arr=true) > duplicate name 1`] = `
+{
+  "Accept": [
+    "text/html",
+    "application/json",
+  ],
+}
+`;
+
+exports[`toV2Headers (arr=true) > empty 1`] = `{}`;
+
+exports[`toV2Headers (arr=true) > empty name skipped 1`] = `
+{
+  "X-Real": [
+    "kept",
+  ],
+}
+`;
+
+exports[`toV2Headers (arr=true) > empty value skipped 1`] = `
+{
+  "X-Real": [
+    "kept",
+  ],
+}
+`;
+
+exports[`toV2Headers (arr=true) > single 1`] = `
+{
+  "Host": [
+    "example.test",
+  ],
+}
+`;

+ 60 - 0
frontend/src/test/__snapshots__/inbound-link.test.ts.snap

@@ -0,0 +1,60 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`genHysteriaLink > hysteria-v1-tls: byte-stable 1`] = `"hysteria://[email protected]:36715?security=tls&fp=chrome&alpn=h3&sni=hysteria.example.test#parity-test"`;
+
+exports[`genInboundLinks orchestrator > hysteria-v1-tls: byte-stable 1`] = `"hysteria://[email protected]:36715?security=tls&fp=chrome&alpn=h3&sni=hysteria.example.test#parity-test-gina%40example.test"`;
+
+exports[`genInboundLinks orchestrator > shadowsocks-tcp-2022: byte-stable 1`] = `"ss://MjAyMi1ibGFrZTMtYWVzLTI1Ni1nY206Wm1GclpTMXpaWEoyWlhJdGNHRnpjM2R2Y21RdE1EQXdNUT09OmRHVnpkQzFqYkdsbGJuUXRjR0Z6YzNkdmNtUXRNUT09@override.test:8388?type=tcp#parity-test-frank%40example.test"`;
+
+exports[`genInboundLinks orchestrator > trojan-ws-tls: byte-stable 1`] = `"trojan://[email protected]:443?type=ws&path=%2Ftrojan&host=trojan.example.test&security=tls&fp=chrome&alpn=h2%2Chttp%2F1.1&sni=trojan.example.test#parity-test-eve%40example.test"`;
+
+exports[`genInboundLinks orchestrator > vless-tcp-reality: byte-stable 1`] = `"vless://[email protected]:443?type=tcp&encryption=none&security=reality&pbk=Tx5yj1bRcOPHkdvT2pIAQ2zh0gQ8m4OPdnzqXJxxV3o&fp=chrome&sid=a3f1&spx=%2F&flow=xtls-rprx-vision#parity-test-dave%40example.test"`;
+
+exports[`genInboundLinks orchestrator > vless-ws-tls: byte-stable 1`] = `"vless://[email protected]:443?type=ws&encryption=none&path=%2Fws&host=cdn.example.test&security=tls&fp=chrome&alpn=h2%2Chttp%2F1.1&sni=cdn.example.test#parity-test-alice%40example.test"`;
+
+exports[`genInboundLinks orchestrator > vmess-tcp-tls: byte-stable 1`] = `"vmess://ewogICJ2IjogIjIiLAogICJwcyI6ICJwYXJpdHktdGVzdC1jYXJvbEBleGFtcGxlLnRlc3QiLAogICJhZGQiOiAib3ZlcnJpZGUudGVzdCIsCiAgInBvcnQiOiA4NDQzLAogICJpZCI6ICIxMTExMTExMS0yMjIyLTQzMzMtODQ0NC01NTU1NTU1NTU1NTUiLAogICJzY3kiOiAiYXV0byIsCiAgIm5ldCI6ICJ0Y3AiLAogICJ0bHMiOiAidGxzIiwKICAidHlwZSI6ICJub25lIiwKICAic25pIjogInZtZXNzLmV4YW1wbGUudGVzdCIsCiAgImZwIjogImNocm9tZSIsCiAgImFscG4iOiAiaDIsaHR0cC8xLjEiCn0="`;
+
+exports[`genInboundLinks orchestrator > wireguard-server: byte-stable 1`] = `
+"[Interface]
+PrivateKey = QGVlb2dXc1ZTWGw0ZXBzZndsWmtMaUM5MUlNYjBHWFdYbz0=
+Address = 10.0.0.2/32
+DNS = 1.1.1.1, 1.0.0.1
+MTU = 1420
+
+# parity-test-1
+[Peer]
+PublicKey = Piehk2n8UewhMHMyJiBS+Sxn/OK0FalyFW1GAGzokHM=
+AllowedIPs = 0.0.0.0/0, ::/0
+Endpoint = override.test:51820
+PersistentKeepalive = 25
+"
+`;
+
+exports[`genShadowsocksLink > shadowsocks-tcp-2022: byte-stable 1`] = `"ss://MjAyMi1ibGFrZTMtYWVzLTI1Ni1nY206Wm1GclpTMXpaWEoyWlhJdGNHRnpjM2R2Y21RdE1EQXdNUT09OmRHVnpkQzFqYkdsbGJuUXRjR0Z6YzNkdmNtUXRNUT09@example.test:8388?type=tcp#parity-test"`;
+
+exports[`genTrojanLink > trojan-ws-tls: byte-stable 1`] = `"trojan://[email protected]:443?type=ws&path=%2Ftrojan&host=trojan.example.test&security=tls&fp=chrome&alpn=h2%2Chttp%2F1.1&sni=trojan.example.test#parity-test"`;
+
+exports[`genVlessLink > vless-tcp-reality: byte-stable 1`] = `"vless://[email protected]:443?type=tcp&encryption=none&security=reality&pbk=Tx5yj1bRcOPHkdvT2pIAQ2zh0gQ8m4OPdnzqXJxxV3o&fp=chrome&sid=a3f1&spx=%2F&flow=xtls-rprx-vision#parity-test"`;
+
+exports[`genVlessLink > vless-ws-tls: byte-stable 1`] = `"vless://[email protected]:443?type=ws&encryption=none&path=%2Fws&host=cdn.example.test&security=tls&fp=chrome&alpn=h2%2Chttp%2F1.1&sni=cdn.example.test#parity-test"`;
+
+exports[`genVmessLink > vmess-tcp-tls: byte-stable 1`] = `"vmess://ewogICJ2IjogIjIiLAogICJwcyI6ICJwYXJpdHktdGVzdCIsCiAgImFkZCI6ICJleGFtcGxlLnRlc3QiLAogICJwb3J0IjogODQ0MywKICAiaWQiOiAiMTExMTExMTEtMjIyMi00MzMzLTg0NDQtNTU1NTU1NTU1NTU1IiwKICAic2N5IjogImF1dG8iLAogICJuZXQiOiAidGNwIiwKICAidGxzIjogInRscyIsCiAgInR5cGUiOiAibm9uZSIsCiAgInNuaSI6ICJ2bWVzcy5leGFtcGxlLnRlc3QiLAogICJmcCI6ICJjaHJvbWUiLAogICJhbHBuIjogImgyLGh0dHAvMS4xIgp9"`;
+
+exports[`genWireguardLink + genWireguardConfig > wireguard-server: byte-stable 1`] = `
+{
+  "config": "[Interface]
+PrivateKey = QGVlb2dXc1ZTWGw0ZXBzZndsWmtMaUM5MUlNYjBHWFdYbz0=
+Address = 10.0.0.2/32
+DNS = 1.1.1.1, 1.0.0.1
+MTU = 1420
+
+# wg-peer-1
+[Peer]
+PublicKey = Piehk2n8UewhMHMyJiBS+Sxn/OK0FalyFW1GAGzokHM=
+AllowedIPs = 0.0.0.0/0, ::/0
+Endpoint = wg.example.test:51820
+PersistentKeepalive = 25
+",
+  "link": "wireguard://QGVlb2dXc1ZTWGw0ZXBzZndsWmtMaUM5MUlNYjBHWFdYbz0%[email protected]:51820?publickey=Piehk2n8UewhMHMyJiBS%2BSxn%2FOK0FalyFW1GAGzokHM%3D&address=10.0.0.2%2F32&mtu=1420#wg-peer-1",
+}
+`;

+ 1681 - 0
frontend/src/test/__snapshots__/protocol-capabilities.test.ts.snap

@@ -0,0 +1,1681 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`protocol capability predicates > http-basic :: grpc/none 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": false,
+  "canEnableTls": false,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > http-basic :: grpc/reality 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": false,
+  "canEnableTls": false,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > http-basic :: grpc/tls 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": false,
+  "canEnableTls": false,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > http-basic :: httpupgrade/none 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": false,
+  "canEnableTls": false,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > http-basic :: httpupgrade/tls 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": false,
+  "canEnableTls": false,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > http-basic :: kcp/none 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": false,
+  "canEnableTls": false,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > http-basic :: tcp/none 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": false,
+  "canEnableTls": false,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > http-basic :: tcp/reality 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": false,
+  "canEnableTls": false,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > http-basic :: tcp/tls 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": false,
+  "canEnableTls": false,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > http-basic :: ws/none 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": false,
+  "canEnableTls": false,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > http-basic :: ws/tls 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": false,
+  "canEnableTls": false,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > http-basic :: xhttp/none 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": false,
+  "canEnableTls": false,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > http-basic :: xhttp/reality 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": false,
+  "canEnableTls": false,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > http-basic :: xhttp/tls 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": false,
+  "canEnableTls": false,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > hysteria-basic :: grpc/none 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": true,
+  "canEnableTls": true,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > hysteria-basic :: grpc/reality 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": true,
+  "canEnableTls": true,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > hysteria-basic :: grpc/tls 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": true,
+  "canEnableTls": true,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > hysteria-basic :: httpupgrade/none 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": true,
+  "canEnableTls": true,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > hysteria-basic :: httpupgrade/tls 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": true,
+  "canEnableTls": true,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > hysteria-basic :: kcp/none 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": true,
+  "canEnableTls": true,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > hysteria-basic :: tcp/none 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": true,
+  "canEnableTls": true,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > hysteria-basic :: tcp/reality 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": true,
+  "canEnableTls": true,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > hysteria-basic :: tcp/tls 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": true,
+  "canEnableTls": true,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > hysteria-basic :: ws/none 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": true,
+  "canEnableTls": true,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > hysteria-basic :: ws/tls 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": true,
+  "canEnableTls": true,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > hysteria-basic :: xhttp/none 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": true,
+  "canEnableTls": true,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > hysteria-basic :: xhttp/reality 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": true,
+  "canEnableTls": true,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > hysteria-basic :: xhttp/tls 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": true,
+  "canEnableTls": true,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > hysteria2-basic :: grpc/none 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": false,
+  "canEnableTls": false,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > hysteria2-basic :: grpc/reality 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": false,
+  "canEnableTls": false,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > hysteria2-basic :: grpc/tls 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": false,
+  "canEnableTls": false,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > hysteria2-basic :: httpupgrade/none 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": false,
+  "canEnableTls": false,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > hysteria2-basic :: httpupgrade/tls 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": false,
+  "canEnableTls": false,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > hysteria2-basic :: kcp/none 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": false,
+  "canEnableTls": false,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > hysteria2-basic :: tcp/none 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": false,
+  "canEnableTls": false,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > hysteria2-basic :: tcp/reality 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": false,
+  "canEnableTls": false,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > hysteria2-basic :: tcp/tls 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": false,
+  "canEnableTls": false,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > hysteria2-basic :: ws/none 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": false,
+  "canEnableTls": false,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > hysteria2-basic :: ws/tls 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": false,
+  "canEnableTls": false,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > hysteria2-basic :: xhttp/none 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": false,
+  "canEnableTls": false,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > hysteria2-basic :: xhttp/reality 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": false,
+  "canEnableTls": false,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > hysteria2-basic :: xhttp/tls 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": false,
+  "canEnableTls": false,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > mixed-basic :: grpc/none 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": false,
+  "canEnableTls": false,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > mixed-basic :: grpc/reality 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": false,
+  "canEnableTls": false,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > mixed-basic :: grpc/tls 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": false,
+  "canEnableTls": false,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > mixed-basic :: httpupgrade/none 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": false,
+  "canEnableTls": false,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > mixed-basic :: httpupgrade/tls 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": false,
+  "canEnableTls": false,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > mixed-basic :: kcp/none 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": false,
+  "canEnableTls": false,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > mixed-basic :: tcp/none 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": false,
+  "canEnableTls": false,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > mixed-basic :: tcp/reality 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": false,
+  "canEnableTls": false,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > mixed-basic :: tcp/tls 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": false,
+  "canEnableTls": false,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > mixed-basic :: ws/none 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": false,
+  "canEnableTls": false,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > mixed-basic :: ws/tls 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": false,
+  "canEnableTls": false,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > mixed-basic :: xhttp/none 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": false,
+  "canEnableTls": false,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > mixed-basic :: xhttp/reality 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": false,
+  "canEnableTls": false,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > mixed-basic :: xhttp/tls 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": false,
+  "canEnableTls": false,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > shadowsocks-2022 :: grpc/none 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": true,
+  "canEnableTls": true,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": true,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > shadowsocks-2022 :: grpc/reality 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": true,
+  "canEnableTls": true,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": true,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > shadowsocks-2022 :: grpc/tls 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": true,
+  "canEnableTls": true,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": true,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > shadowsocks-2022 :: httpupgrade/none 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": true,
+  "canEnableTls": true,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": true,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > shadowsocks-2022 :: httpupgrade/tls 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": true,
+  "canEnableTls": true,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": true,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > shadowsocks-2022 :: kcp/none 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": true,
+  "canEnableTls": false,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": true,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > shadowsocks-2022 :: tcp/none 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": true,
+  "canEnableTls": true,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": true,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > shadowsocks-2022 :: tcp/reality 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": true,
+  "canEnableTls": true,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": true,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > shadowsocks-2022 :: tcp/tls 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": true,
+  "canEnableTls": true,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": true,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > shadowsocks-2022 :: ws/none 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": true,
+  "canEnableTls": true,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": true,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > shadowsocks-2022 :: ws/tls 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": true,
+  "canEnableTls": true,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": true,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > shadowsocks-2022 :: xhttp/none 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": true,
+  "canEnableTls": true,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": true,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > shadowsocks-2022 :: xhttp/reality 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": true,
+  "canEnableTls": true,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": true,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > shadowsocks-2022 :: xhttp/tls 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": true,
+  "canEnableTls": true,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": true,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > trojan-basic :: grpc/none 1`] = `
+{
+  "canEnableReality": true,
+  "canEnableStream": true,
+  "canEnableTls": true,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > trojan-basic :: grpc/reality 1`] = `
+{
+  "canEnableReality": true,
+  "canEnableStream": true,
+  "canEnableTls": true,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > trojan-basic :: grpc/tls 1`] = `
+{
+  "canEnableReality": true,
+  "canEnableStream": true,
+  "canEnableTls": true,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > trojan-basic :: httpupgrade/none 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": true,
+  "canEnableTls": true,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > trojan-basic :: httpupgrade/tls 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": true,
+  "canEnableTls": true,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > trojan-basic :: kcp/none 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": true,
+  "canEnableTls": false,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > trojan-basic :: tcp/none 1`] = `
+{
+  "canEnableReality": true,
+  "canEnableStream": true,
+  "canEnableTls": true,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > trojan-basic :: tcp/reality 1`] = `
+{
+  "canEnableReality": true,
+  "canEnableStream": true,
+  "canEnableTls": true,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > trojan-basic :: tcp/tls 1`] = `
+{
+  "canEnableReality": true,
+  "canEnableStream": true,
+  "canEnableTls": true,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > trojan-basic :: ws/none 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": true,
+  "canEnableTls": true,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > trojan-basic :: ws/tls 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": true,
+  "canEnableTls": true,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > trojan-basic :: xhttp/none 1`] = `
+{
+  "canEnableReality": true,
+  "canEnableStream": true,
+  "canEnableTls": true,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > trojan-basic :: xhttp/reality 1`] = `
+{
+  "canEnableReality": true,
+  "canEnableStream": true,
+  "canEnableTls": true,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > trojan-basic :: xhttp/tls 1`] = `
+{
+  "canEnableReality": true,
+  "canEnableStream": true,
+  "canEnableTls": true,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > tunnel-basic :: grpc/none 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": false,
+  "canEnableTls": false,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > tunnel-basic :: grpc/reality 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": false,
+  "canEnableTls": false,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > tunnel-basic :: grpc/tls 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": false,
+  "canEnableTls": false,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > tunnel-basic :: httpupgrade/none 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": false,
+  "canEnableTls": false,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > tunnel-basic :: httpupgrade/tls 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": false,
+  "canEnableTls": false,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > tunnel-basic :: kcp/none 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": false,
+  "canEnableTls": false,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > tunnel-basic :: tcp/none 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": false,
+  "canEnableTls": false,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > tunnel-basic :: tcp/reality 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": false,
+  "canEnableTls": false,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > tunnel-basic :: tcp/tls 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": false,
+  "canEnableTls": false,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > tunnel-basic :: ws/none 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": false,
+  "canEnableTls": false,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > tunnel-basic :: ws/tls 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": false,
+  "canEnableTls": false,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > tunnel-basic :: xhttp/none 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": false,
+  "canEnableTls": false,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > tunnel-basic :: xhttp/reality 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": false,
+  "canEnableTls": false,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > tunnel-basic :: xhttp/tls 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": false,
+  "canEnableTls": false,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > vless-tcp-none :: grpc/none 1`] = `
+{
+  "canEnableReality": true,
+  "canEnableStream": true,
+  "canEnableTls": true,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > vless-tcp-none :: grpc/reality 1`] = `
+{
+  "canEnableReality": true,
+  "canEnableStream": true,
+  "canEnableTls": true,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > vless-tcp-none :: grpc/tls 1`] = `
+{
+  "canEnableReality": true,
+  "canEnableStream": true,
+  "canEnableTls": true,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > vless-tcp-none :: httpupgrade/none 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": true,
+  "canEnableTls": true,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > vless-tcp-none :: httpupgrade/tls 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": true,
+  "canEnableTls": true,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > vless-tcp-none :: kcp/none 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": true,
+  "canEnableTls": false,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > vless-tcp-none :: tcp/none 1`] = `
+{
+  "canEnableReality": true,
+  "canEnableStream": true,
+  "canEnableTls": true,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > vless-tcp-none :: tcp/reality 1`] = `
+{
+  "canEnableReality": true,
+  "canEnableStream": true,
+  "canEnableTls": true,
+  "canEnableTlsFlow": true,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > vless-tcp-none :: tcp/tls 1`] = `
+{
+  "canEnableReality": true,
+  "canEnableStream": true,
+  "canEnableTls": true,
+  "canEnableTlsFlow": true,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > vless-tcp-none :: ws/none 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": true,
+  "canEnableTls": true,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > vless-tcp-none :: ws/tls 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": true,
+  "canEnableTls": true,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > vless-tcp-none :: xhttp/none 1`] = `
+{
+  "canEnableReality": true,
+  "canEnableStream": true,
+  "canEnableTls": true,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > vless-tcp-none :: xhttp/reality 1`] = `
+{
+  "canEnableReality": true,
+  "canEnableStream": true,
+  "canEnableTls": true,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > vless-tcp-none :: xhttp/tls 1`] = `
+{
+  "canEnableReality": true,
+  "canEnableStream": true,
+  "canEnableTls": true,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > vmess-basic :: grpc/none 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": true,
+  "canEnableTls": true,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > vmess-basic :: grpc/reality 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": true,
+  "canEnableTls": true,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > vmess-basic :: grpc/tls 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": true,
+  "canEnableTls": true,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > vmess-basic :: httpupgrade/none 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": true,
+  "canEnableTls": true,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > vmess-basic :: httpupgrade/tls 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": true,
+  "canEnableTls": true,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > vmess-basic :: kcp/none 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": true,
+  "canEnableTls": false,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > vmess-basic :: tcp/none 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": true,
+  "canEnableTls": true,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > vmess-basic :: tcp/reality 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": true,
+  "canEnableTls": true,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > vmess-basic :: tcp/tls 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": true,
+  "canEnableTls": true,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > vmess-basic :: ws/none 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": true,
+  "canEnableTls": true,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > vmess-basic :: ws/tls 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": true,
+  "canEnableTls": true,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > vmess-basic :: xhttp/none 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": true,
+  "canEnableTls": true,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > vmess-basic :: xhttp/reality 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": true,
+  "canEnableTls": true,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > vmess-basic :: xhttp/tls 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": true,
+  "canEnableTls": true,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > wireguard-basic :: grpc/none 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": false,
+  "canEnableTls": false,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > wireguard-basic :: grpc/reality 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": false,
+  "canEnableTls": false,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > wireguard-basic :: grpc/tls 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": false,
+  "canEnableTls": false,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > wireguard-basic :: httpupgrade/none 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": false,
+  "canEnableTls": false,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > wireguard-basic :: httpupgrade/tls 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": false,
+  "canEnableTls": false,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > wireguard-basic :: kcp/none 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": false,
+  "canEnableTls": false,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > wireguard-basic :: tcp/none 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": false,
+  "canEnableTls": false,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > wireguard-basic :: tcp/reality 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": false,
+  "canEnableTls": false,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > wireguard-basic :: tcp/tls 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": false,
+  "canEnableTls": false,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > wireguard-basic :: ws/none 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": false,
+  "canEnableTls": false,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > wireguard-basic :: ws/tls 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": false,
+  "canEnableTls": false,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > wireguard-basic :: xhttp/none 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": false,
+  "canEnableTls": false,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > wireguard-basic :: xhttp/reality 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": false,
+  "canEnableTls": false,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;
+
+exports[`protocol capability predicates > wireguard-basic :: xhttp/tls 1`] = `
+{
+  "canEnableReality": false,
+  "canEnableStream": false,
+  "canEnableTls": false,
+  "canEnableTlsFlow": false,
+  "canEnableVisionSeed": false,
+  "isSS2022": false,
+  "isSSMultiUser": true,
+}
+`;

+ 10 - 9
frontend/src/test/headers.test.ts

@@ -1,10 +1,11 @@
 import { describe, expect, it } from 'vitest';
 
 import { getHeaderValue, toHeaders, toV2Headers, type HeaderEntry } from '@/lib/xray/headers';
-import { XrayCommonClass } from '@/models/inbound';
 
-// Shadow harness: the new pure helpers must agree byte-for-byte with the
-// legacy XrayCommonClass static methods. Drift here is a regression.
+// Pure-function tests for the header helpers. Snapshots were locked at
+// the close of the legacy class migration — at that point toHeaders and
+// toV2Headers were verified byte-equal to the legacy XrayCommonClass
+// static methods. Drift past this baseline is a regression.
 
 const headerMapCases: Array<[string, unknown]> = [
   ['null', null],
@@ -17,10 +18,10 @@ const headerMapCases: Array<[string, unknown]> = [
   ['mixed', { Host: 'a.example.test', 'X-Trace': ['1', '2'] }],
 ];
 
-describe('toHeaders parity with XrayCommonClass.toHeaders', () => {
+describe('toHeaders', () => {
   for (const [label, input] of headerMapCases) {
     it(label, () => {
-      expect(toHeaders(input)).toEqual(XrayCommonClass.toHeaders(input));
+      expect(toHeaders(input)).toMatchSnapshot();
     });
   }
 });
@@ -42,18 +43,18 @@ const entryCases: Array<[string, HeaderEntry[]]> = [
   ]],
 ];
 
-describe('toV2Headers parity (arr=true)', () => {
+describe('toV2Headers (arr=true)', () => {
   for (const [label, input] of entryCases) {
     it(label, () => {
-      expect(toV2Headers(input, true)).toEqual(XrayCommonClass.toV2Headers(input, true));
+      expect(toV2Headers(input, true)).toMatchSnapshot();
     });
   }
 });
 
-describe('toV2Headers parity (arr=false)', () => {
+describe('toV2Headers (arr=false)', () => {
   for (const [label, input] of entryCases) {
     it(label, () => {
-      expect(toV2Headers(input, false)).toEqual(XrayCommonClass.toV2Headers(input, false));
+      expect(toV2Headers(input, false)).toMatchSnapshot();
     });
   }
 });

+ 68 - 123
frontend/src/test/inbound-link.test.ts

@@ -12,18 +12,13 @@ import {
   genWireguardLink,
   resolveAddr,
 } from '@/lib/xray/inbound-link';
-import { Inbound as LegacyInbound } from '@/models/inbound';
 import { InboundSchema } from '@/schemas/api/inbound';
 import type { WireguardInboundSettings } from '@/schemas/protocols/inbound/wireguard';
 
-// Parity harness for the share-link extraction. For each full inbound
-// fixture matching the protocol under test, we:
-//   1. Parse with the Zod InboundSchema -> typed input for the new pure fn
-//   2. Construct the legacy Inbound class via Inbound.fromJson(fixture)
-//   3. Call both link generators with matching args
-//   4. Assert the URLs match byte-for-byte
-// Drift between the new pure fn and the legacy class method fails the
-// test here, before the call sites in pages/ get swapped.
+// Snapshot baseline for the share-link generators. Snapshots were locked
+// at the close of the legacy class migration — at that point each
+// generator was verified byte-equal to the corresponding legacy Inbound
+// class method. Future drift past this baseline is a regression.
 
 const fullFixtures = import.meta.glob<unknown>(
   './golden/fixtures/inbound-full/*.json',
@@ -42,140 +37,108 @@ function fixturesForProtocol(protocol: string): Array<[string, Record<string, un
     .sort(([a], [b]) => a.localeCompare(b));
 }
 
-describe('genVmessLink parity', () => {
+describe('genVmessLink', () => {
   const fixtures = fixturesForProtocol('vmess');
   expect(fixtures.length, 'need at least one vmess full-inbound fixture').toBeGreaterThan(0);
 
   for (const [name, raw] of fixtures) {
-    it(`${name}: matches legacy Inbound.genVmessLink`, () => {
+    it(`${name}: byte-stable`, () => {
       const typed = InboundSchema.parse(raw);
       const settings = (raw as { settings: { clients: Array<{ id: string; security?: string }> } }).settings;
       const client = settings.clients[0];
 
-      const address = 'example.test';
-      const port = typed.port;
-      const remark = 'parity-test';
-
-      const newLink = genVmessLink({
+      const link = genVmessLink({
         inbound: typed,
-        address,
-        port,
+        address: 'example.test',
+        port: typed.port,
         forceTls: 'same',
-        remark,
+        remark: 'parity-test',
         clientId: client.id,
         security: client.security as never,
         externalProxy: null,
       });
-
-      const legacy = LegacyInbound.fromJson(raw);
-      const legacyLink = legacy.genVmessLink(address, port, 'same', remark, client.id, client.security, null);
-
-      expect(newLink).toBe(legacyLink);
+      expect(link).toMatchSnapshot();
     });
   }
 });
 
-describe('genVlessLink parity', () => {
+describe('genVlessLink', () => {
   const fixtures = fixturesForProtocol('vless');
   expect(fixtures.length, 'need at least one vless full-inbound fixture').toBeGreaterThan(0);
 
   for (const [name, raw] of fixtures) {
-    it(`${name}: matches legacy Inbound.genVLESSLink`, () => {
+    it(`${name}: byte-stable`, () => {
       const typed = InboundSchema.parse(raw);
       const settings = (raw as { settings: { clients: Array<{ id: string; flow?: string }> } }).settings;
       const client = settings.clients[0];
 
-      const address = 'example.test';
-      const port = typed.port;
-      const remark = 'parity-test';
-
-      const newLink = genVlessLink({
+      const link = genVlessLink({
         inbound: typed,
-        address,
-        port,
+        address: 'example.test',
+        port: typed.port,
         forceTls: 'same',
-        remark,
+        remark: 'parity-test',
         clientId: client.id,
         flow: client.flow as never,
         externalProxy: null,
       });
-
-      const legacy = LegacyInbound.fromJson(raw);
-      const legacyLink = legacy.genVLESSLink(address, port, 'same', remark, client.id, client.flow, null);
-
-      expect(newLink).toBe(legacyLink);
+      expect(link).toMatchSnapshot();
     });
   }
 });
 
-describe('genTrojanLink parity', () => {
+describe('genTrojanLink', () => {
   const fixtures = fixturesForProtocol('trojan');
   expect(fixtures.length, 'need at least one trojan full-inbound fixture').toBeGreaterThan(0);
 
   for (const [name, raw] of fixtures) {
-    it(`${name}: matches legacy Inbound.genTrojanLink`, () => {
+    it(`${name}: byte-stable`, () => {
       const typed = InboundSchema.parse(raw);
       const settings = (raw as { settings: { clients: Array<{ password: string }> } }).settings;
       const client = settings.clients[0];
 
-      const address = 'example.test';
-      const port = typed.port;
-      const remark = 'parity-test';
-
-      const newLink = genTrojanLink({
+      const link = genTrojanLink({
         inbound: typed,
-        address,
-        port,
+        address: 'example.test',
+        port: typed.port,
         forceTls: 'same',
-        remark,
+        remark: 'parity-test',
         clientPassword: client.password,
         externalProxy: null,
       });
-
-      const legacy = LegacyInbound.fromJson(raw);
-      const legacyLink = legacy.genTrojanLink(address, port, 'same', remark, client.password, null);
-
-      expect(newLink).toBe(legacyLink);
+      expect(link).toMatchSnapshot();
     });
   }
 });
 
-describe('genHysteriaLink parity', () => {
+describe('genHysteriaLink', () => {
   const fixtures = fixturesForProtocol('hysteria');
   expect(fixtures.length, 'need at least one hysteria full-inbound fixture').toBeGreaterThan(0);
 
   for (const [name, raw] of fixtures) {
-    it(`${name}: matches legacy Inbound.genHysteriaLink`, () => {
+    it(`${name}: byte-stable`, () => {
       const typed = InboundSchema.parse(raw);
       const settings = (raw as { settings: { clients: Array<{ auth: string }> } }).settings;
       const client = settings.clients[0];
 
-      const address = 'example.test';
-      const port = typed.port;
-      const remark = 'parity-test';
-
-      const newLink = genHysteriaLink({
+      const link = genHysteriaLink({
         inbound: typed,
-        address,
-        port,
-        remark,
+        address: 'example.test',
+        port: typed.port,
+        remark: 'parity-test',
         clientAuth: client.auth,
       });
-
-      const legacy = LegacyInbound.fromJson(raw);
-      const legacyLink = legacy.genHysteriaLink(address, port, remark, client.auth);
-
-      expect(newLink).toBe(legacyLink);
+      expect(link).toMatchSnapshot();
     });
   }
 });
 
-describe('genWireguardLink + genWireguardConfig parity', () => {
+describe('genWireguardLink + genWireguardConfig', () => {
   const fixtures = fixturesForProtocol('wireguard');
   expect(fixtures.length, 'need at least one wireguard full-inbound fixture').toBeGreaterThan(0);
 
   for (const [name, raw] of fixtures) {
-    it(`${name}: matches legacy getWireguardLink + getWireguardTxt`, () => {
+    it(`${name}: byte-stable`, () => {
       const typed = InboundSchema.parse(raw);
       if (typed.protocol !== 'wireguard') throw new Error('not a wireguard fixture');
       // InboundSchema is an intersection of two DUs, so TS can't auto-narrow
@@ -183,20 +146,21 @@ describe('genWireguardLink + genWireguardConfig parity', () => {
       // check; this cast just helps the type checker.
       const settings = typed.settings as WireguardInboundSettings;
 
-      const address = 'wg.example.test';
-      const port = typed.port;
-      const remark = 'wg-peer-1';
-      const peerIndex = 0;
-
-      const newLink = genWireguardLink({ settings, address, port, remark, peerIndex });
-      const newConfig = genWireguardConfig({ settings, address, port, remark, peerIndex });
-
-      const legacy = LegacyInbound.fromJson(raw);
-      const legacyLink = legacy.getWireguardLink(address, port, remark, peerIndex);
-      const legacyConfig = legacy.getWireguardTxt(address, port, remark, peerIndex);
-
-      expect(newLink).toBe(legacyLink);
-      expect(newConfig).toBe(legacyConfig);
+      const link = genWireguardLink({
+        settings,
+        address: 'wg.example.test',
+        port: typed.port,
+        remark: 'wg-peer-1',
+        peerIndex: 0,
+      });
+      const config = genWireguardConfig({
+        settings,
+        address: 'wg.example.test',
+        port: typed.port,
+        remark: 'wg-peer-1',
+        peerIndex: 0,
+      });
+      expect({ link, config }).toMatchSnapshot();
     });
   }
 });
@@ -241,72 +205,53 @@ describe('resolveAddr precedence', () => {
   });
 });
 
-describe('genInboundLinks orchestrator parity', () => {
+describe('genInboundLinks orchestrator', () => {
   // Every full-inbound fixture should produce the same \r\n-joined link
-  // block as the legacy Inbound.genInboundLinks. Pass hostOverride
-  // explicitly so neither pipeline reaches for location.hostname.
+  // block at this baseline.
   const fixtures = Object.entries(fullFixtures)
     .map(([path, raw]): [string, Record<string, unknown>] => [fixtureName(path), raw as Record<string, unknown>])
     .sort(([a], [b]) => a.localeCompare(b));
 
   for (const [name, raw] of fixtures) {
     const protocol = (raw as { protocol?: string }).protocol;
-    // Skip protocols the legacy class can't dispatch (hysteria2 has no
-    // dispatch case; getSettings(protocol) returns null and crashes
-    // genHysteriaLink). Orchestrator-level parity covers the others.
+    // Skip hysteria2 — the legacy class had no dispatch case at the time
+    // the baseline was locked, so no snapshot exists. The new orchestrator
+    // covers it via its own logic and the genHysteriaLink unit test.
     if (protocol === 'hysteria2') continue;
 
-    it(`${name}: matches legacy Inbound.genInboundLinks`, () => {
+    it(`${name}: byte-stable`, () => {
       const typed = InboundSchema.parse(raw);
-
-      const remark = 'parity-test';
-      const hostOverride = 'override.test';
-      const fallbackHostname = 'fallback.test';
-
-      const newBlock = genInboundLinks({
+      const block = genInboundLinks({
         inbound: typed,
-        remark,
-        hostOverride,
-        fallbackHostname,
+        remark: 'parity-test',
+        hostOverride: 'override.test',
+        fallbackHostname: 'fallback.test',
       });
-
-      const legacy = LegacyInbound.fromJson(raw);
-      const legacyBlock = legacy.genInboundLinks(remark, '-ieo', hostOverride);
-
-      expect(newBlock).toBe(legacyBlock);
+      expect(block).toMatchSnapshot();
     });
   }
 });
 
-describe('genShadowsocksLink parity', () => {
+describe('genShadowsocksLink', () => {
   const fixtures = fixturesForProtocol('shadowsocks');
   expect(fixtures.length, 'need at least one shadowsocks full-inbound fixture').toBeGreaterThan(0);
 
   for (const [name, raw] of fixtures) {
-    it(`${name}: matches legacy Inbound.genSSLink`, () => {
+    it(`${name}: byte-stable`, () => {
       const typed = InboundSchema.parse(raw);
       const settings = (raw as { settings: { clients?: Array<{ password: string }> } }).settings;
       const client = settings.clients?.[0];
 
-      const address = 'example.test';
-      const port = typed.port;
-      const remark = 'parity-test';
-      const clientPassword = client?.password ?? '';
-
-      const newLink = genShadowsocksLink({
+      const link = genShadowsocksLink({
         inbound: typed,
-        address,
-        port,
+        address: 'example.test',
+        port: typed.port,
         forceTls: 'same',
-        remark,
-        clientPassword,
+        remark: 'parity-test',
+        clientPassword: client?.password ?? '',
         externalProxy: null,
       });
-
-      const legacy = LegacyInbound.fromJson(raw);
-      const legacyLink = legacy.genSSLink(address, port, 'same', remark, clientPassword, null);
-
-      expect(newLink).toBe(legacyLink);
+      expect(link).toMatchSnapshot();
     });
   }
 });

+ 15 - 26
frontend/src/test/protocol-capabilities.test.ts

@@ -1,7 +1,6 @@
 /// <reference types="vite/client" />
 import { describe, expect, it } from 'vitest';
 
-import { Inbound } from '@/models/inbound';
 import {
   canEnableTls,
   canEnableReality,
@@ -12,13 +11,10 @@ import {
   isSSMultiUser,
 } from '@/lib/xray/protocol-capabilities';
 
-// Parity harness for the capability predicates. For each golden fixture
-// (protocol+settings), cross with a matrix of stream configurations
-// (network × security), build the legacy Inbound class via fromJson, and
-// assert each pure-function predicate matches the class method.
-//
-// Only the (protocol × stream-shape) cross matters here — the predicates
-// never read sniffing/port/listen, so we hold those constant.
+// Pure-function tests for the capability predicates. Each fixture × stream
+// case is locked via snapshot — these were captured at the close of the
+// legacy class migration and verified byte-equal to the legacy Inbound
+// class instance methods. Drift past this baseline is a regression.
 
 const fixtures = import.meta.glob<unknown>(
   './golden/fixtures/inbound/*.json',
@@ -48,7 +44,7 @@ function fixtureName(path: string): string {
   return (path.split('/').pop() ?? path).replace(/\.json$/, '');
 }
 
-describe('protocol capability predicates: pure ↔ legacy parity', () => {
+describe('protocol capability predicates', () => {
   const entries = Object.entries(fixtures).sort(([a], [b]) => a.localeCompare(b));
   for (const [path, raw] of entries) {
     const name = fixtureName(path);
@@ -57,28 +53,21 @@ describe('protocol capability predicates: pure ↔ legacy parity', () => {
     for (const stream of STREAM_CASES) {
 
       it(`${name} :: ${stream.network}/${stream.security}`, () => {
-        const wireConfig = {
-          port: 12345,
-          listen: '127.0.0.1',
-          protocol: fix.protocol,
-          settings: fix.settings,
-          streamSettings: { network: stream.network, security: stream.security },
-          sniffing: {},
-        };
-        const legacy = Inbound.fromJson(wireConfig);
         const values = {
           protocol: fix.protocol,
           streamSettings: { network: stream.network, security: stream.security },
           settings: fix.settings,
         };
-
-        expect(canEnableTls(values)).toBe(legacy.canEnableTls());
-        expect(canEnableReality(values)).toBe(legacy.canEnableReality());
-        expect(canEnableTlsFlow(values)).toBe(legacy.canEnableTlsFlow());
-        expect(canEnableStream(values)).toBe(legacy.canEnableStream());
-        expect(canEnableVisionSeed(values)).toBe(legacy.canEnableVisionSeed());
-        expect(isSS2022(values)).toBe(legacy.isSS2022);
-        expect(isSSMultiUser(values)).toBe(legacy.isSSMultiUser);
+        const result = {
+          canEnableTls: canEnableTls(values),
+          canEnableReality: canEnableReality(values),
+          canEnableTlsFlow: canEnableTlsFlow(values),
+          canEnableStream: canEnableStream(values),
+          canEnableVisionSeed: canEnableVisionSeed(values),
+          isSS2022: isSS2022(values),
+          isSSMultiUser: isSSMultiUser(values),
+        };
+        expect(result).toMatchSnapshot();
       });
     }
   }

+ 0 - 78
frontend/src/test/shadow.test.ts

@@ -1,78 +0,0 @@
-/// <reference types="vite/client" />
-import { describe, expect, it } from 'vitest';
-
-import { Inbound } from '@/models/inbound';
-import { InboundSettingsSchema } from '@/schemas/protocols';
-
-// Walks every inbound golden fixture through both pipelines:
-//   OLD:   Inbound.Settings.fromJson(protocol, raw.settings).toJson()
-//   NEW:   InboundSettingsSchema.parse(raw).settings
-// Then canonicalizes (deep key-sort, undefined-strip via JSON round-trip)
-// and asserts byte-equality. This is the safety net for Step 3d — once we
-// start extracting class methods into lib/xray/* pure functions, any
-// normalization drift trips a snapshot diff here.
-
-const fixtures = import.meta.glob<unknown>(
-  './golden/fixtures/inbound/*.json',
-  { eager: true, import: 'default' },
-);
-
-type FixtureShape = { protocol: string; settings: unknown };
-
-// The OLD panel class collapses hysteria + hysteria2 onto a single
-// HysteriaSettings (distinguished only by `version`), so when a fixture
-// carries the wire-level hysteria2 protocol literal we dispatch to the
-// HYSTERIA branch on the legacy side.
-function legacyProtocolFor(protocol: string): string {
-  if (protocol === 'hysteria2') return 'hysteria';
-  return protocol;
-}
-
-// Drops empty arrays and undefined/null fields, then sorts keys. The legacy
-// class's toJson() omits optional fields whose value is the empty array
-// (e.g. fallbacks: []); the Zod schema includes them because of .default([]).
-// Both represent the same wire state, so we treat them as equivalent here.
-function canonicalize(value: unknown): string {
-  function normalize(v: unknown): unknown {
-    if (Array.isArray(v)) {
-      const items = v.map(normalize).filter((x) => x !== undefined);
-      return items.length === 0 ? undefined : items;
-    }
-    if (v && typeof v === 'object') {
-      const entries = Object.entries(v as Record<string, unknown>)
-        .map(([k, val]) => [k, normalize(val)] as const)
-        .filter(([, val]) => val !== undefined && val !== null)
-        .sort(([a], [b]) => a.localeCompare(b));
-      return entries.length === 0 ? undefined : Object.fromEntries(entries);
-    }
-    return v;
-  }
-  return JSON.stringify(normalize(value) ?? null);
-}
-
-function fixtureName(path: string): string {
-  const file = path.split('/').pop() ?? path;
-  return file.replace(/\.json$/, '');
-}
-
-describe('shadow parse: legacy class vs Zod schema', () => {
-  const entries = Object.entries(fixtures).sort(([a], [b]) => a.localeCompare(b));
-
-  for (const [path, raw] of entries) {
-    const fixture = raw as FixtureShape;
-    const name = fixtureName(path);
-
-    it(`${name}: legacy toJson() and Zod parse converge`, () => {
-      const legacyInstance = Inbound.Settings.fromJson(
-        legacyProtocolFor(fixture.protocol),
-        fixture.settings,
-      );
-      expect(legacyInstance, `legacy dispatch returned null for ${fixture.protocol}`).not.toBeNull();
-      const legacyJson = legacyInstance.toJson();
-
-      const zodParsed = InboundSettingsSchema.parse(fixture);
-
-      expect(canonicalize(zodParsed.settings)).toBe(canonicalize(legacyJson));
-    });
-  }
-});