#!/usr/bin/env bash set -eo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" STABLE_REF="${TACHYON_STABLE_REF:-0.7.19.9}" STABLE_REPO="${TACHYON_STABLE_REPO:-}" MATRIX_SCRIPT="$ROOT_DIR/tests/helpers/config_contract_matrix.js" WORK_DIR="$(mktemp -d)" LEGACY_STEM="$(printf '\160\157\144\153\157\160')" cleanup() { rm -rf "$WORK_DIR" } trap cleanup EXIT fail() { printf 'FAIL: %s\n' "$1" >&2 exit 1 } ensure_stable_ref() { if git -C "$ROOT_DIR" rev-parse --verify "$STABLE_REF^{commit}" >/dev/null 2>&1; then return 0 fi git -C "$ROOT_DIR" fetch --force --depth=1 origin "refs/tags/$STABLE_REF:refs/tags/$STABLE_REF" >/dev/null 2>&1 || fail "stable ref is unavailable and could not be fetched: $STABLE_REF" } prepare_stable_repo() { if [ -n "$STABLE_REPO" ]; then if [ ! -r "$STABLE_REPO/tachyon/files/etc/config/tachyon" ] && [ ! -r "$STABLE_REPO/$LEGACY_STEM/files/etc/config/$LEGACY_STEM" ]; then fail "stable repo is missing the expected config template: $STABLE_REPO" fi return 0 fi ensure_stable_ref STABLE_REPO="$WORK_DIR/stable-$STABLE_REF" mkdir -p "$STABLE_REPO" git -C "$ROOT_DIR" archive "$STABLE_REF" | tar -x -C "$STABLE_REPO" || fail "failed to materialize stable baseline: $STABLE_REF" } prepare_stable_repo node "$MATRIX_SCRIPT" --current "$ROOT_DIR" --stable "$STABLE_REPO" >"$WORK_DIR/matrix.json" node - "$WORK_DIR/matrix.json" <<'NODE' const fs = require("fs"); const matrix = JSON.parse(fs.readFileSync(process.argv[2], "utf8")); function fail(message) { console.error(message); process.exit(1); } const retiredMissingCurrent = new Set([ "urltest_hide_filtered_outbounds", ]); const missing = matrix.fields.filter((field) => field.status === "missing_current" && !retiredMissingCurrent.has(field.name)); if (missing.length) { fail(`stable config fields missing in current contract: ${missing.map((field) => field.name).join(", ")}`); } for (const name of ["dns_type", "subscription_urls", "selector_proxy_links", "action", "protocol", "transport", "security", "user_domains", "user_domains_text", "user_domain_list_type", "local_domain_lists", "remote_domain_lists", "remote_subnet_lists", "domain_ip_lists", "fully_routed_ips", "server_uuid", "tailscale_auth_key"]) { const field = matrix.fields.find((item) => item.name === name); if (!field) fail(`expected config field is absent from matrix: ${name}`); if (field.status !== "supported" && field.status !== "migrated") { fail(`expected config field ${name} to be supported or migrated, got ${field.status}`); } } if ((matrix.summary.supported || 0) < 140) { fail(`unexpectedly small supported config surface: ${matrix.summary.supported || 0}`); } if (matrix.stable.version !== "0.7.19.9") { fail(`unexpected stable baseline: ${matrix.stable.version}`); } function uiValues(field) { const result = new Set(); const entries = (field && field.ui) || []; for (const entry of entries) { for (const value of entry.values || []) { result.add(value); } } return [...result].sort(); } function assertCurrentKeepsStableValues(name, required = [], retired = []) { const field = matrix.fields.find((item) => item.name === name); if (!field) fail(`expected enum field is absent from matrix: ${name}`); const stableValues = uiValues(field.stable); const currentValues = uiValues(field.current); const missingStable = stableValues.filter((value) => !currentValues.includes(value) && !retired.includes(value)); if (missingStable.length) { fail(`current UI contract for ${name} is missing stable values: ${missingStable.join(", ")}`); } const missingRequired = required.filter((value) => !currentValues.includes(value)); if (missingRequired.length) { fail(`current UI contract for ${name} is missing required values: ${missingRequired.join(", ")}`); } } assertCurrentKeepsStableValues("action", ["connection", "bypass", "block", "zapret", "zapret2", "byedpi", "dns"], ["direct", "proxy", "vpn", "outbound"]); assertCurrentKeepsStableValues("protocol", ["tailscale", "vless", "vmess", "trojan", "shadowsocks", "hysteria2", "socks", "mtproto", "json_inbound"]); assertCurrentKeepsStableValues("security", ["reality", "tls", "none"]); assertCurrentKeepsStableValues("transport", ["tcp", "ws", "grpc", "http", "httpupgrade", "xhttp"]); assertCurrentKeepsStableValues("urltest_filter_mode", ["disabled", "exclude", "include", "mixed"]); assertCurrentKeepsStableValues("dns_type", ["doh", "dot", "udp"]); assertCurrentKeepsStableValues("routing_mode", ["rules", "direct", "section"]); assertCurrentKeepsStableValues("shadowsocks_method", ["aes-128-gcm", "aes-256-gcm", "chacha20-ietf-poly1305"]); assertCurrentKeepsStableValues("vless_flow", ["none", "xtls-rprx-vision"]); assertCurrentKeepsStableValues("transport_xhttp_mode", ["auto", "packet-up", "stream-up", "stream-one"]); NODE printf 'config contract matrix checks passed\n'