Files
NeoTachyon/tests/subscription_cache_state.sh
T

520 lines
22 KiB
Bash

#!/usr/bin/env bash
set -eo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
CACHE_UC="$ROOT_DIR/tachyon/files/usr/lib/subscription/cache.uc"
TACHYON_LIB="$ROOT_DIR/tachyon/files/usr/lib"
WORK_DIR="$(mktemp -d)"
cleanup() {
rm -rf "$WORK_DIR"
}
trap cleanup EXIT
fail() {
printf 'FAIL: %s\n' "$1" >&2
exit 1
}
assert_eq() {
local expected="$1"
local actual="$2"
local label="$3"
[ "$actual" = "$expected" ] || fail "$label: expected '$expected', got '$actual'"
}
cache_ucode() {
ucode -L "$TACHYON_LIB" "$CACHE_UC" "$@"
}
assert_eq "first second" \
"$(cache_ucode append-state-list-once "first" second)" \
"append new state item"
assert_eq "first second" \
"$(cache_ucode append-state-list-once "first second" second)" \
"do not append duplicate state item"
assert_eq "first second" \
"$(cache_ucode append-state-list-once " first second " "")" \
"empty state item keeps normalized list"
cache_ucode state-list-contains "first second" second >/dev/null ||
fail "state list should contain second"
if cache_ucode state-list-contains "first second" third >/dev/null 2>&1; then
fail "state list should not contain third"
fi
SUBSCRIPTION_RUNTIME_SH="$ROOT_DIR/tachyon/files/usr/lib/subscription_runtime.sh"
UPDATES_RUNTIME_SH="$ROOT_DIR/tachyon/files/usr/lib/updates_runtime.sh"
[ ! -e "$SUBSCRIPTION_RUNTIME_SH" ] ||
fail "subscription_runtime.sh shell owner must be removed"
[ ! -e "$UPDATES_RUNTIME_SH" ] ||
fail "updates_runtime.sh shell owner must be removed"
if grep -R -n "subscription_runtime.sh" "$ROOT_DIR/tachyon/files" >/dev/null 2>&1; then
fail "runtime files must not reference subscription_runtime.sh"
fi
subscription_runtime_shell_symbols='subscription_runtime_|prepare_subscription_caches_for_startup|prepare_subscription_caches_for_runtime_generation|run_deferred_subscription_bootstrap|stop_deferred_subscription_bootstrap_retry_worker'
if grep -R -n -E "$subscription_runtime_shell_symbols" "$ROOT_DIR/tachyon/files/usr/bin/tachyon" "$ROOT_DIR/tachyon/files/usr/lib" --include='*.sh' >/dev/null 2>&1; then
fail "subscription_runtime shell symbols must not remain in runtime shell"
fi
if grep -R -n -E 'get_subscription_metadata_path|get_outbound_metadata_path' "$ROOT_DIR/tachyon/files/usr/bin/tachyon" "$ROOT_DIR/tachyon/files/usr/lib" --include='*.sh' >/dev/null 2>&1; then
fail "subscription metadata path helpers must be owned by subscription/cache.uc"
fi
if grep -n -E 'require\("uci"\)\.cursor|uci -q' "$CACHE_UC" >/dev/null 2>&1; then
fail "subscription/cache.uc must use core.uci instead of owning direct UCI cursor or CLI calls"
fi
move_file_source="$(sed -n '/^function move_file(/,/^}/p' "$CACHE_UC")"
if grep -Fq 'unlink_path(target)' <<<"$move_file_source"; then
fail "subscription cache replacement must keep the current file until atomic rename succeeds"
fi
if grep -Fq 'remove-legacy-server-country-cache' "$CACHE_UC"; then
fail "subscription/cache.uc must not expose the migrated legacy cache cleanup"
fi
grep -q 'mode == "ensure-runtime-dirs"' "$CACHE_UC" ||
fail "subscription/cache.uc must expose ensure-runtime-dirs owner mode"
grep -q 'mode == "ensure-runtime-cache-format"' "$CACHE_UC" ||
fail "subscription/cache.uc must expose ensure-runtime-cache-format owner mode"
grep -q 'mode == "prepare-caches"' "$CACHE_UC" ||
fail "subscription/cache.uc must expose prepare-caches owner mode"
grep -q 'mode == "update-source"' "$CACHE_UC" ||
fail "subscription/cache.uc must expose update-source owner mode"
grep -q 'mode == "update-request"' "$CACHE_UC" ||
fail "subscription/cache.uc must expose update-request owner mode"
grep -q 'mode == "update-section"' "$CACHE_UC" ||
fail "subscription/cache.uc must expose update-section owner mode"
grep -q 'mode == "section-is-subscription-proxy"' "$CACHE_UC" ||
fail "subscription/cache.uc must expose subscription proxy predicate mode"
grep -q 'mode == "subscription-metadata-path"' "$CACHE_UC" ||
fail "subscription/cache.uc must expose subscription metadata path mode"
grep -q 'mode == "outbound-metadata-path"' "$CACHE_UC" ||
fail "subscription/cache.uc must expose outbound metadata path mode"
grep -q 'mode == "run-deferred-bootstrap"' "$CACHE_UC" ||
fail "subscription/cache.uc must expose deferred bootstrap owner mode"
grep -q 'mode == "stop-deferred-bootstrap-worker"' "$CACHE_UC" ||
fail "subscription/cache.uc must expose deferred worker stop owner mode"
assert_eq "proxy-subscription-2" \
"$(cache_ucode source-id proxy 2)" \
"source id mode"
assert_eq "/var/run/tachyon/subscription-metadata/proxy.json" \
"$(cache_ucode subscription-metadata-path proxy)" \
"subscription metadata path mode"
assert_eq "/var/run/tachyon/outbound-metadata/proxy.json" \
"$(cache_ucode outbound-metadata-path proxy)" \
"outbound metadata path mode"
if cache_ucode subscription-metadata-path '../bad' >/dev/null 2>&1; then
fail "subscription metadata path mode should reject unsafe section names"
fi
cat >"$WORK_DIR/normalized-subscription.json" <<'JSON'
{
"version": 1,
"format": "uri-list",
"skipped": 2,
"outbounds": [
{ "type": "vless", "tag": "one" },
{ "type": "trojan", "tag": "two" }
]
}
JSON
assert_eq "2 proxy entries, 2 skipped entries" \
"$(cache_ucode subscription-import-stats "$WORK_DIR/normalized-subscription.json")" \
"subscription import stats"
assert_eq "Subscription source 1 for rule 'proxy' imported: 2 proxy entries, 2 skipped entries" \
"$(cache_ucode subscription-source-summary proxy 1 "$WORK_DIR/normalized-subscription.json" imported)" \
"subscription source imported summary"
assert_eq "Subscription source 1 for rule 'proxy' is unchanged: 2 proxy entries, 2 skipped entries" \
"$(cache_ucode subscription-source-summary proxy 1 "$WORK_DIR/normalized-subscription.json" unchanged)" \
"subscription source unchanged summary"
runtime_env() {
TMP_SING_BOX_FOLDER="$WORK_DIR/runtime/tmp-sing-box" \
TMP_RULESET_FOLDER="$WORK_DIR/runtime/tmp-sing-box/rulesets" \
TMP_SUBSCRIPTION_FOLDER="$WORK_DIR/runtime/tmp-sing-box/subscriptions" \
TACHYON_RUNTIME_STATE_DIR="$WORK_DIR/runtime/run" \
TACHYON_SUBSCRIPTION_UPDATE_STATE_DIR="$WORK_DIR/runtime/run/subscription-update" \
TACHYON_SUBSCRIPTION_LINKS_DIR="$WORK_DIR/runtime/run/subscription-links" \
TACHYON_SUBSCRIPTION_METADATA_DIR="$WORK_DIR/runtime/run/subscription-metadata" \
TACHYON_OUTBOUND_METADATA_DIR="$WORK_DIR/runtime/run/outbound-metadata" \
TACHYON_SECTION_CACHE_DIR="$WORK_DIR/runtime/run/section-cache" \
TACHYON_RUNTIME_CACHE_FORMAT_FILE="$WORK_DIR/runtime/run/cache-format" \
TACHYON_PERSISTENT_SUBSCRIPTION_CACHE_DIR="$WORK_DIR/runtime/persistent" \
TACHYON_PERSISTENT_SUBSCRIPTION_CACHE_FORMAT_FILE="$WORK_DIR/runtime/persistent/cache-format" \
TACHYON_PERSISTENT_SUBSCRIPTION_CACHE_FORMAT="7" \
TACHYON_SUBSCRIPTION_BOOTSTRAP_RETRY_PID_FILE="$WORK_DIR/runtime/run/bootstrap.pid" \
"$@"
}
runtime_env cache_ucode ensure-runtime-dirs
for dir in \
"$WORK_DIR/runtime/tmp-sing-box" \
"$WORK_DIR/runtime/tmp-sing-box/rulesets" \
"$WORK_DIR/runtime/tmp-sing-box/subscriptions" \
"$WORK_DIR/runtime/run" \
"$WORK_DIR/runtime/run/subscription-update" \
"$WORK_DIR/runtime/run/subscription-metadata" \
"$WORK_DIR/runtime/run/outbound-metadata" \
"$WORK_DIR/runtime/run/section-cache"; do
[ -d "$dir" ] || fail "ensure-runtime-dirs should create $dir"
done
if [ -d "$WORK_DIR/runtime/run/subscription-links" ]; then
fail "ensure-runtime-dirs should not recreate the retired subscription-links directory"
fi
mkdir -p "$WORK_DIR/runtime/run/subscription-links" "$WORK_DIR/runtime/persistent"
printf '7\n' >"$WORK_DIR/runtime/run/cache-format"
printf 'stale\n' >"$WORK_DIR/runtime/run/section-cache/stale.json"
printf 'stale\n' >"$WORK_DIR/runtime/run/subscription-links/stale.json"
printf '7\n' >"$WORK_DIR/runtime/persistent/cache-format"
cat >"$WORK_DIR/runtime/persistent/proxy-subscription-1.json" <<'JSON'
{
"version": 1,
"format": "sing-box-json",
"outbounds": [
{
"type": "socks",
"tag": "preserved-node",
"server": "127.0.0.1",
"server_port": 1080
}
]
}
JSON
runtime_env cache_ucode ensure-runtime-cache-format
assert_eq "8" \
"$(sed -n '1p' "$WORK_DIR/runtime/run/cache-format")" \
"runtime cache format"
[ ! -e "$WORK_DIR/runtime/run/section-cache/stale.json" ] ||
fail "ensure-runtime-cache-format should clear old runtime section cache"
[ ! -e "$WORK_DIR/runtime/run/subscription-links" ] ||
fail "runtime format migration should remove the retired subscription-links directory"
assert_eq "7" \
"$(sed -n '1p' "$WORK_DIR/runtime/persistent/cache-format")" \
"persistent cache format"
[ -f "$WORK_DIR/runtime/persistent/proxy-subscription-1.json" ] ||
fail "runtime format migration should preserve the compatible persistent subscription cache"
node - "$WORK_DIR/runtime/persistent/proxy-subscription-1.json" <<'NODE'
const fs = require('fs');
const subscription = JSON.parse(fs.readFileSync(process.argv[2], 'utf8'));
const outbound = subscription.outbounds[0];
if (outbound.server !== '127.0.0.1' || outbound.share_link !== 'socks5://127.0.0.1:1080#preserved-node') {
console.error('runtime format migration should preserve the outbound and backfill its direct link');
process.exit(1);
}
NODE
runtime_env cache_ucode run-deferred-bootstrap ""
runtime_env cache_ucode stop-deferred-bootstrap-worker
mkdir -p \
"$WORK_DIR/tmp" \
"$WORK_DIR/persistent" \
"$WORK_DIR/sections" \
"$WORK_DIR/links" \
"$WORK_DIR/metadata" \
"$WORK_DIR/outbound"
cat >"$WORK_DIR/cache-maintenance.json" <<'JSON'
{
"section": [
{
".name": "proxy",
"enabled": "1",
"action": "proxy",
"subscription_urls": [
"https://example.com/one.txt",
"https://example.com/two.txt"
]
},
{
".name": "direct",
"enabled": "1",
"action": "bypass"
},
{
".name": "disabled",
"enabled": "0",
"action": "proxy",
"subscription_urls": [
"https://example.com/disabled.txt"
]
},
{
".name": "bad/name",
"enabled": "1",
"action": "proxy",
"subscription_urls": [
"https://example.com/bad.txt"
]
}
]
}
JSON
assert_eq $'sections\tproxy direct disabled\nsources\tproxy-subscription-1 proxy-subscription-2 disabled-subscription-1\nall\tproxy proxy-subscription-1 proxy-subscription-2 direct disabled disabled-subscription-1\nmissing\t1' \
"$(cache_ucode maintenance-plan-fixture "$WORK_DIR/cache-maintenance.json" "$WORK_DIR/sections")" \
"cache maintenance plan with missing proxy section cache"
touch "$WORK_DIR/sections/proxy.json"
assert_eq $'sections\tproxy direct disabled\nsources\tproxy-subscription-1 proxy-subscription-2 disabled-subscription-1\nall\tproxy proxy-subscription-1 proxy-subscription-2 direct disabled disabled-subscription-1\nmissing\t0' \
"$(cache_ucode maintenance-plan-fixture "$WORK_DIR/cache-maintenance.json" "$WORK_DIR/sections")" \
"cache maintenance plan with available proxy section cache"
cache_ucode runtime-cache-needs-rebuild-fixture "$WORK_DIR/cache-maintenance.json" "$WORK_DIR/sections" >/dev/null &&
fail "runtime cache should not need rebuild after proxy cache exists"
cache_ucode prepared-runtime-cache-should-skip-fixture "$WORK_DIR/cache-maintenance.json" "$WORK_DIR/sections" runtime 1 >/dev/null ||
fail "prepared runtime cache should skip only when section cache exists"
rm -f "$WORK_DIR/sections/proxy.json"
cache_ucode runtime-cache-needs-rebuild-fixture "$WORK_DIR/cache-maintenance.json" "$WORK_DIR/sections" >/dev/null ||
fail "runtime cache should need rebuild when proxy cache is missing"
if cache_ucode prepared-runtime-cache-should-skip-fixture "$WORK_DIR/cache-maintenance.json" "$WORK_DIR/sections" runtime 1 >/dev/null 2>&1; then
fail "prepared runtime cache must not skip when section cache is missing"
fi
touch \
"$WORK_DIR/tmp/proxy-subscription-1.json" \
"$WORK_DIR/tmp/stale-subscription-1.json" \
"$WORK_DIR/persistent/proxy-subscription-2.url" \
"$WORK_DIR/persistent/stale-subscription-2.user_agent" \
"$WORK_DIR/sections/proxy.json" \
"$WORK_DIR/sections/proxy-subscription-1.json" \
"$WORK_DIR/sections/stale.json" \
"$WORK_DIR/links/proxy.json" \
"$WORK_DIR/links/proxy-subscription-1.json" \
"$WORK_DIR/metadata/direct.metadata.json" \
"$WORK_DIR/outbound/stale.json" \
"$WORK_DIR/outbound/cache-format"
candidate_paths="$(
printf '%s\n' \
"$WORK_DIR/tmp/proxy-subscription-1.json" \
"$WORK_DIR/tmp/stale-subscription-1.json" \
"$WORK_DIR/persistent/proxy-subscription-2.url" \
"$WORK_DIR/persistent/stale-subscription-2.user_agent" \
"$WORK_DIR/sections/proxy.json" \
"$WORK_DIR/sections/proxy-subscription-1.json" \
"$WORK_DIR/sections/stale.json" \
"$WORK_DIR/links/proxy.json" \
"$WORK_DIR/links/proxy-subscription-1.json" \
"$WORK_DIR/metadata/direct.metadata.json" \
"$WORK_DIR/outbound/stale.json" \
"$WORK_DIR/outbound/cache-format"
)"
assert_eq "$(
printf '%s\n' \
"$WORK_DIR/tmp/stale-subscription-1.json" \
"$WORK_DIR/persistent/stale-subscription-2.user_agent" \
"$WORK_DIR/sections/stale.json" \
"$WORK_DIR/links/proxy-subscription-1.json" \
"$WORK_DIR/outbound/stale.json"
)" \
"$(printf '%s\n' "$candidate_paths" | cache_ucode stale-cache-delete-paths-fixture \
"$WORK_DIR/cache-maintenance.json" \
"$WORK_DIR/tmp" \
"$WORK_DIR/persistent" \
"$WORK_DIR/sections" \
"$WORK_DIR/links" \
"$WORK_DIR/metadata" \
"$WORK_DIR/outbound")" \
"stale cache delete path plan"
mkdir -p "$WORK_DIR/runtime-cache" "$WORK_DIR/persistent-cache"
cat >"$WORK_DIR/cache-current.json" <<'JSON'
{
"section": [
{
".name": "proxy",
"subscription_urls": [
"https://example.com/one.txt",
"https://example.com/two.txt"
],
"subscription_url_settings": "{\"https://example.com/one.txt\":{\"user_agent\":\"v2rayN\"}}"
},
{
".name": "manual",
"selector_proxy_links": "vless://manual"
}
]
}
JSON
cat >"$WORK_DIR/runtime-cache/proxy-subscription-1.json" <<'JSON'
{"outbounds":[{"type":"direct","tag":"one"}]}
JSON
printf '%s' 'https://example.com/one.txt' >"$WORK_DIR/runtime-cache/proxy-subscription-1.url"
printf '%s' 'v2rayN' >"$WORK_DIR/runtime-cache/proxy-subscription-1.user_agent"
cache_ucode section-current-usable-cache-fixture \
"$WORK_DIR/cache-current.json" proxy "$WORK_DIR/runtime-cache" "$WORK_DIR/persistent-cache" "sing-box/default" >/dev/null ||
fail "runtime cache should be current and usable"
printf '%s' 'stale-hwid' >"$WORK_DIR/runtime-cache/proxy-subscription-1.hwid"
if cache_ucode section-current-usable-cache-fixture \
"$WORK_DIR/cache-current.json" proxy "$WORK_DIR/runtime-cache" "$WORK_DIR/persistent-cache" "sing-box/default" >"$WORK_DIR/hwid.out" 2>"$WORK_DIR/hwid.err"; then
fail "mismatched generated HWID cache should not be current"
fi
if grep -Fq "left-hand side is not a function" "$WORK_DIR/hwid.err"; then
fail "mismatched generated HWID cache must not crash"
fi
rm -f "$WORK_DIR/runtime-cache/proxy-subscription-1.hwid"
mkdir -p "$WORK_DIR/custom-header-cache"
cat >"$WORK_DIR/cache-custom-headers.json" <<'JSON'
{
"section": [
{
".name": "custom",
"subscription_urls": [
"https://example.com/custom.txt"
],
"subscription_url_settings": "{\"https://example.com/custom.txt\":{\"user_agent\":\"v2rayN\",\"custom_device_headers\":\"1\",\"device_os\":\"iOS\",\"ver_os\":\"26.4\",\"device_model\":\"iPhone 17 Pro Max\",\"device_locale\":\"ru\",\"app_version\":\"4.6.0\",\"accept_language\":\"ru-RU,ru;q=0.9\"}}"
}
]
}
JSON
cat >"$WORK_DIR/custom-header-cache/custom-subscription-1.json" <<'JSON'
{"outbounds":[{"type":"direct","tag":"custom"}]}
JSON
printf '%s' 'https://example.com/custom.txt' >"$WORK_DIR/custom-header-cache/custom-subscription-1.url"
printf '%s' 'v2rayN' >"$WORK_DIR/custom-header-cache/custom-subscription-1.user_agent"
if cache_ucode section-current-usable-cache-fixture \
"$WORK_DIR/cache-custom-headers.json" custom "$WORK_DIR/custom-header-cache" "$WORK_DIR/persistent-cache" "sing-box/default" >/dev/null 2>&1; then
fail "custom device headers should invalidate a cache without their signature"
fi
cat >"$WORK_DIR/custom-header-cache/custom-subscription-1.json" <<'JSON'
{"outbounds":[{"type":"direct","tag":"custom"}]}
JSON
printf '%s' 'https://example.com/custom.txt' >"$WORK_DIR/custom-header-cache/custom-subscription-1.url"
printf '%s' 'v2rayN' >"$WORK_DIR/custom-header-cache/custom-subscription-1.user_agent"
printf '%s' '{ "enabled": "1", "device_os": "iOS", "ver_os": "26.4", "device_model": "iPhone 17 Pro Max", "device_locale": "ru", "app_version": "4.6.0", "accept_language": "ru-RU,ru;q=0.9" }' >"$WORK_DIR/custom-header-cache/custom-subscription-1.device_headers"
cache_ucode section-current-usable-cache-fixture \
"$WORK_DIR/cache-custom-headers.json" custom "$WORK_DIR/custom-header-cache" "$WORK_DIR/persistent-cache" "sing-box/default" >/dev/null ||
fail "matching custom device headers signature should keep the cache current"
sed 's/\\"device_os\\":\\"iOS\\"/\\"device_os\\":\\"Android\\"/' \
"$WORK_DIR/cache-custom-headers.json" >"$WORK_DIR/cache-custom-headers-changed.json"
if cache_ucode section-current-usable-cache-fixture \
"$WORK_DIR/cache-custom-headers-changed.json" custom "$WORK_DIR/custom-header-cache" "$WORK_DIR/persistent-cache" "sing-box/default" >/dev/null 2>&1; then
fail "changed custom device headers should invalidate cache"
fi
printf '%s' 'https://example.com/stale.txt' >"$WORK_DIR/runtime-cache/proxy-subscription-1.url"
if cache_ucode section-current-usable-cache-fixture \
"$WORK_DIR/cache-current.json" proxy "$WORK_DIR/runtime-cache" "$WORK_DIR/persistent-cache" "sing-box/default" >/dev/null 2>&1; then
fail "stale runtime cache URL should not be current"
fi
rm -f "$WORK_DIR/runtime-cache/proxy-subscription-1.json" "$WORK_DIR/runtime-cache/proxy-subscription-1.url" "$WORK_DIR/runtime-cache/proxy-subscription-1.user_agent"
cat >"$WORK_DIR/persistent-cache/proxy-subscription-2.json" <<'JSON'
{"outbounds":[{"type":"direct","tag":"two"}]}
JSON
printf '%s' 'https://example.com/two.txt' >"$WORK_DIR/persistent-cache/proxy-subscription-2.url"
printf '%s' 'sing-box/default' >"$WORK_DIR/persistent-cache/proxy-subscription-2.user_agent"
cache_ucode section-current-usable-cache-fixture \
"$WORK_DIR/cache-current.json" proxy "$WORK_DIR/runtime-cache" "$WORK_DIR/persistent-cache" "sing-box/default" >/dev/null ||
fail "persistent cache should restore and satisfy current cache"
test -s "$WORK_DIR/runtime-cache/proxy-subscription-2.json" ||
fail "persistent cache restore should write runtime json"
assert_eq 'https://example.com/two.txt' \
"$(cat "$WORK_DIR/runtime-cache/proxy-subscription-2.url")" \
"restored subscription URL"
if cache_ucode section-current-usable-cache-fixture \
"$WORK_DIR/cache-current.json" manual "$WORK_DIR/runtime-cache" "$WORK_DIR/persistent-cache" "sing-box/default" >/dev/null 2>&1; then
fail "section without subscription URLs should not have usable subscription cache"
fi
mkdir -p "$WORK_DIR/link-subscriptions"
cat >"$WORK_DIR/link-subscriptions/proxy-subscription-1.json" <<'JSON'
{
"outbounds": [
{
"type": "vless",
"tag": "vless-encrypted",
"server": "example.com",
"server_port": 443,
"uuid": "00000000-0000-4000-8000-000000000001",
"encryption": "mlkem768x25519plus.native.test"
},
{
"type": "vless",
"tag": "vless-none",
"server": "example.com",
"server_port": 443,
"uuid": "00000000-0000-4000-8000-000000000001",
"encryption": "none"
}
]
}
JSON
serialize_outbound_link() {
ucode -L "$TACHYON_LIB" -e '
let fs = require("fs");
let share_link = require("subscription.share_link");
let source = json(fs.readfile(ARGV[0]));
print(share_link.serialize_outbound_link(source.outbounds[int(ARGV[1])]), "\n");
' "$1" "$2"
}
encrypted_link="$(serialize_outbound_link "$WORK_DIR/link-subscriptions/proxy-subscription-1.json" 0)"
case "$encrypted_link" in
*encryption=mlkem768x25519plus.native.test*) ;;
*) fail "expected VLESS encryption in serialized link, got $encrypted_link" ;;
esac
none_link="$(serialize_outbound_link "$WORK_DIR/link-subscriptions/proxy-subscription-1.json" 1)"
case "$none_link" in
*encryption=none*) fail "did not expect encryption=none in serialized link, got $none_link" ;;
esac
mkdir -p "$WORK_DIR/persist-runtime" "$WORK_DIR/persist-empty" "$WORK_DIR/persist-fresh"
cat >"$WORK_DIR/persist-runtime/proxy-subscription-1.json" <<'JSON'
{"outbounds":[{"type":"direct","tag":"one"}]}
JSON
cat >"$WORK_DIR/runtime/persistent/proxy-subscription-1.metadata.json" <<'JSON'
{"version":1,"title":"WolfPN"}
JSON
printf '%s' 'https://example.com/one.txt' >"$WORK_DIR/runtime/persistent/proxy-subscription-1.url"
printf '%s' 'v2rayN' >"$WORK_DIR/runtime/persistent/proxy-subscription-1.user_agent"
cat >"$WORK_DIR/persist-empty/empty.metadata.json" <<'JSON'
{}
JSON
runtime_env cache_ucode persist-source-cache \
proxy-subscription-1 \
"$WORK_DIR/persist-runtime/proxy-subscription-1.json" \
'https://example.com/one.txt' \
'v2rayN' \
"$WORK_DIR/persist-empty/empty.metadata.json"
test -s "$WORK_DIR/runtime/persistent/proxy-subscription-1.metadata.json" ||
fail "same subscription source with empty fresh metadata must keep previous metadata"
node - "$WORK_DIR/runtime/persistent/proxy-subscription-1.metadata.json" <<'JS'
const fs = require("fs");
const value = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
if (value.title !== "WolfPN") {
console.error("previous metadata was not preserved");
process.exit(1);
}
JS
runtime_env cache_ucode persist-source-cache \
proxy-subscription-1 \
"$WORK_DIR/persist-runtime/proxy-subscription-1.json" \
'https://example.com/changed.txt' \
'v2rayN' \
"$WORK_DIR/persist-empty/empty.metadata.json"
[ ! -e "$WORK_DIR/runtime/persistent/proxy-subscription-1.metadata.json" ] ||
fail "changed subscription URL with empty metadata must drop stale metadata"
printf 'subscription cache state checks passed\n'