#!/usr/bin/env bash set -eo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" TACHYON_LIB="$ROOT_DIR/tachyon/files/usr/lib" UPDATES_UC="$ROOT_DIR/tachyon/files/usr/lib/components/updates.uc" fail() { printf 'FAIL: %s\n' "$1" >&2 exit 1 } assert_status() { local expected="$1" local label="$2" shift 2 local status=0 updates_ucode update-is-due "$@" >/dev/null 2>&1 || status="$?" [ "$status" = "$expected" ] || fail "$label: expected exit $expected, got $status" } updates_ucode() { ucode -L "$TACHYON_LIB" "$UPDATES_UC" "$@" } if grep -n -E 'require\("uci"\)\.cursor|uci -q' "$UPDATES_UC" >/dev/null 2>&1; then fail "components/updates.uc must use core.uci instead of owning direct UCI cursor or CLI calls" fi assert_updates_status() { local expected="$1" local label="$2" shift 2 local status=0 updates_ucode "$@" >/dev/null 2>&1 || status="$?" [ "$status" = "$expected" ] || fail "$label: expected exit $expected, got $status" } assert_status 0 "never-run update should be due" 100 0 60 assert_status 0 "expired update should be due" 100 30 60 assert_status 1 "fresh update should not be due" 100 50 60 assert_status 1 "future timestamp should not be due" 100 130 60 assert_status 0 "invalid last-run timestamp should be treated as never-run" 100 invalid 60 assert_status 2 "invalid current timestamp should fail" invalid 0 60 assert_status 2 "invalid interval should fail" 100 0 invalid assert_status 2 "zero interval should fail" 100 0 0 assert_eq() { local expected="$1" local actual="$2" local label="$3" [ "$actual" = "$expected" ] || fail "$label: expected '$expected', got '$actual'" } assert_eq 180 \ "$(updates_ucode duration-to-seconds 3m)" \ "minutes duration" assert_eq 5400 \ "$(updates_ucode duration-to-seconds 1.5h)" \ "decimal duration" if updates_ucode duration-to-seconds invalid >/dev/null 2>&1; then fail "invalid duration should fail" fi assert_eq "* * * * *" \ "$(updates_ucode due-check-cron-schedule 60)" \ "minute cron schedule" assert_eq "0 * * * *" \ "$(updates_ucode due-check-cron-schedule 3600)" \ "hour cron schedule" assert_eq "0 */2 * * *" \ "$(updates_ucode due-check-cron-schedule 7200)" \ "two-hour cron schedule" assert_eq "*/30 * * * *" \ "$(updates_ucode due-check-cron-schedule 1800)" \ "half-hour cron schedule" assert_eq "0 0 * * *" \ "$(updates_ucode due-check-cron-schedule 86400)" \ "daily cron schedule" assert_eq "*/30 * * * * /usr/bin/tachyon list_update_if_due # list" \ "$(updates_ucode list-update-cron-job 30m /usr/bin/tachyon '# list')" \ "list update cron job" assert_eq $'https://raw.githubusercontent.com/itdoginfo/allow-domains/main/Subnets/IPv4/telegram.lst\nhttps://raw.githubusercontent.com/itdoginfo/allow-domains/main/Subnets/IPv6/telegram.lst' \ "$(updates_ucode builtin-subnet-urls telegram)" \ "Telegram built-in subnet families" assert_eq 'https://raw.githubusercontent.com/itdoginfo/allow-domains/main/Subnets/IPv4/roblox.lst' \ "$(updates_ucode builtin-subnet-urls roblox)" \ "Roblox available subnet family" assert_eq "0 */2 * * * /usr/bin/tachyon subscription_update_if_due # subscription" \ "$(updates_ucode subscription-update-cron-job 7200 /usr/bin/tachyon '# subscription')" \ "subscription update cron job" assert_eq $'error\trule3\tinvalid\nmin\t1800' \ "$(printf 'rule1\t2h\nrule2\t30m\nrule3\tinvalid\nrule4\t\n' | updates_ucode subscription-update-interval-plan)" \ "subscription interval plan" assert_eq $'min\t0' \ "$(printf 'rule1\t\n' | updates_ucode subscription-update-interval-plan)" \ "empty subscription interval plan" if updates_ucode list-update-cron-job invalid /usr/bin/tachyon '# list' >/dev/null 2>&1; then fail "invalid list update interval should fail" fi if updates_ucode subscription-update-cron-job 0 /usr/bin/tachyon '# subscription' >/dev/null 2>&1; then fail "invalid subscription update interval should fail" fi WORK_DIR="$(mktemp -d)" cleanup() { rm -rf "$WORK_DIR" } trap cleanup EXIT cat >"$WORK_DIR/cron-plan.json" <<'JSON' { "settings": { "list_update_enabled": "1", "update_interval": "30m", "component_update_check_enabled": "1", "component_update_check_interval": "2h" }, "section": [ { ".name": "list_rule", "enabled": "1", "action": "proxy", "community_lists": [ "telegram" ], "subscription_urls": [ "https://example.com/sub.txt" ], "subscription_update_interval": "2h" }, { ".name": "fast_sub", "enabled": "1", "action": "proxy", "subscription_urls": [ "https://example.com/fast.txt" ], "subscription_update_interval": "45m" }, { ".name": "bad_sub", "enabled": "1", "action": "proxy", "subscription_urls": [ "https://example.com/bad.txt" ], "subscription_update_interval": "bad" }, { ".name": "disabled", "enabled": "0", "action": "proxy", "remote_domain_lists": [ "https://example.com/disabled.lst" ], "subscription_urls": [ "https://example.com/disabled.txt" ] } ] } JSON assert_eq $'list\t*/30 * * * * /usr/bin/tachyon list_update_if_due # list\nsubscription-error\tbad_sub\tbad\nsubscription\t*/45 * * * * /usr/bin/tachyon subscription_update_if_due # subscription\ncomponent\t0 */2 * * * /usr/bin/tachyon component_updates_if_due # component' \ "$(updates_ucode cron-refresh-plan-fixture "$WORK_DIR/cron-plan.json" /usr/bin/tachyon '# list' '# subscription' '# component')" \ "cron refresh plan" grep -Fq 'fs.writefile(tmp, as_string(text)) == null' "$UPDATES_UC" || fail "empty crontab writes must not be treated as fs.writefile failure" cat >"$WORK_DIR/cron-plan-all-presets.json" <<'JSON' { "settings": { "list_update_enabled": "1", "update_interval": "1h", "download_all_presets": "1" }, "section": [] } JSON assert_eq $'list\t0 * * * * /usr/bin/tachyon list_update_if_due # list' \ "$(updates_ucode cron-refresh-plan-fixture "$WORK_DIR/cron-plan-all-presets.json" /usr/bin/tachyon '# list' '# sub' '# comp')" \ "cron refresh plan with download_all_presets" cat >"$WORK_DIR/cron-plan-list-disabled.json" <<'JSON' { "settings": { "list_update_enabled": "0", "update_interval": "30m" }, "section": [ { ".name": "list_rule", "enabled": "1", "action": "proxy", "remote_domain_lists": [ "https://example.com/list.txt" ] } ] } JSON assert_eq "list-disabled" \ "$(updates_ucode cron-refresh-plan-fixture "$WORK_DIR/cron-plan-list-disabled.json" /usr/bin/tachyon '# list' '# subscription' '# component')" \ "disabled list cron plan" cat >"$WORK_DIR/cron-plan-auto-update.json" <<'JSON' { "settings": { "list_update_enabled": "0", "component_update_check_enabled": "1", "component_update_check_interval": "1d", "component_auto_update_enabled": "1", "component_auto_update_mode": "schedule", "component_auto_update_time": "03:30" }, "section": [] } JSON assert_eq $'component\t0 0 * * * /usr/bin/tachyon component_updates_if_due # component\ncomponent-auto-update\t30 3 * * * /usr/bin/tachyon component_auto_update_apply # component' \ "$(updates_ucode cron-refresh-plan-fixture "$WORK_DIR/cron-plan-auto-update.json" /usr/bin/tachyon '# list' '# subscription' '# component')" \ "component auto-update cron plan" cat >"$WORK_DIR/cron-plan-invalid-list.json" <<'JSON' { "settings": { "list_update_enabled": "1", "update_interval": "bad" }, "section": [ { ".name": "list_rule", "enabled": "1", "action": "proxy", "remote_domain_lists": [ "https://example.com/list.txt" ] } ] } JSON status=0 invalid_plan="$(updates_ucode cron-refresh-plan-fixture "$WORK_DIR/cron-plan-invalid-list.json" /usr/bin/tachyon '# list' '# subscription' '# component')" || status="$?" assert_eq 1 "$status" "invalid list cron plan status" assert_eq $'list-error\tbad' "$invalid_plan" "invalid list cron plan output" cat >"$WORK_DIR/existing.cron" <<'CRON' 0 1 * * * /bin/true # keep * * * * * /usr/bin/tachyon list_update_if_due # list * * * * * /usr/bin/tachyon subscription_update_if_due # subscription * * * * * /usr/bin/tachyon component_updates_if_due # component CRON updates_ucode refresh-cron-fixture "$WORK_DIR/cron-plan.json" "$WORK_DIR/existing.cron" /usr/bin/tachyon '# list' '# subscription' '# component' >"$WORK_DIR/cron-apply.json" node - "$WORK_DIR/cron-apply.json" <<'JS' const fs = require("fs"); const value = JSON.parse(fs.readFileSync(process.argv[2], "utf8")); const expected = [ "0 1 * * * /bin/true # keep", "*/30 * * * * /usr/bin/tachyon list_update_if_due # list", "*/45 * * * * /usr/bin/tachyon subscription_update_if_due # subscription", "0 */2 * * * /usr/bin/tachyon component_updates_if_due # component", "" ].join("\n"); if (value.crontab !== expected) { console.error("unexpected applied crontab", JSON.stringify(value.crontab)); process.exit(1); } const messages = value.logs.map(item => item.message); if (!messages.includes("The cron job removed") || !messages.includes("The cron job has been created: */30 * * * * /usr/bin/tachyon list_update_if_due # list") || !messages.includes("The subscription cron job has been created: */45 * * * * /usr/bin/tachyon subscription_update_if_due # subscription") || !messages.includes("The component update check cron job has been created: 0 */2 * * * /usr/bin/tachyon component_updates_if_due # component")) { console.error("unexpected cron apply logs", JSON.stringify(value.logs)); process.exit(1); } JS status=0 updates_ucode refresh-cron-fixture "$WORK_DIR/cron-plan-invalid-list.json" "$WORK_DIR/existing.cron" /usr/bin/tachyon '# list' '# subscription' '# component' >"$WORK_DIR/cron-apply-invalid.json" || status="$?" assert_eq 1 "$status" "invalid cron apply status" node - "$WORK_DIR/cron-apply-invalid.json" <<'JS' const fs = require("fs"); const value = JSON.parse(fs.readFileSync(process.argv[2], "utf8")); if (value.crontab !== "0 1 * * * /bin/true # keep\n") { console.error("invalid plan should remove old tachyon cron jobs only", JSON.stringify(value.crontab)); process.exit(1); } if (!value.logs.some(item => item.level === "error" && item.message === "Invalid update_interval value: bad")) { console.error("invalid plan error log missing", JSON.stringify(value.logs)); process.exit(1); } JS LIST_TIMESTAMP="$WORK_DIR/list.timestamp" SUB_TIMESTAMP="$WORK_DIR/sub.timestamp" rm -f "$LIST_TIMESTAMP" "$SUB_TIMESTAMP" assert_updates_status 0 "list update should be due without timestamp" \ list-update-due-status-fixture "$WORK_DIR/cron-plan.json" "$LIST_TIMESTAMP" 1000 printf '900\n' >"$LIST_TIMESTAMP" assert_updates_status 1 "list update should not be due before interval" \ list-update-due-status-fixture "$WORK_DIR/cron-plan.json" "$LIST_TIMESTAMP" 1000 assert_updates_status 1 "disabled list update should not be due" \ list-update-due-status-fixture "$WORK_DIR/cron-plan-list-disabled.json" "$LIST_TIMESTAMP" 1000 status=0 invalid_due="$(updates_ucode list-update-due-status-fixture "$WORK_DIR/cron-plan-invalid-list.json" "$LIST_TIMESTAMP" 1000)" || status="$?" assert_eq 2 "$status" "invalid list due status" assert_eq $'error\tbad' "$invalid_due" "invalid list due output" rm -f "$SUB_TIMESTAMP" assert_updates_status 0 "subscription update should be due without timestamp" \ subscription-update-section-due-status-fixture "$WORK_DIR/cron-plan.json" fast_sub "$SUB_TIMESTAMP" 1000 printf '900\n' >"$SUB_TIMESTAMP" assert_updates_status 1 "subscription update should not be due before interval" \ subscription-update-section-due-status-fixture "$WORK_DIR/cron-plan.json" fast_sub "$SUB_TIMESTAMP" 1000 assert_updates_status 1 "missing subscription section should not be due" \ subscription-update-section-due-status-fixture "$WORK_DIR/cron-plan.json" missing "$SUB_TIMESTAMP" 1000 status=0 invalid_due="$(updates_ucode subscription-update-section-due-status-fixture "$WORK_DIR/cron-plan.json" bad_sub "$SUB_TIMESTAMP" 1000)" || status="$?" assert_eq 2 "$status" "invalid subscription due status" assert_eq $'error\tbad' "$invalid_due" "invalid subscription due output" printf 'updates due checks passed\n'