Files
NeoTachyon/tests/ui_runtime_job.sh
T
Dushnilin bb1de1fd83 Merge branch 'temp-upstream-rebrand' into main
Resolves merge conflicts:
- install.sh, build.sh, .github/workflows/build.yml: Reverted to Dushnilin's versions to avoid installing SourceForge mirrors.
- fe-app-tachyon: Resolved type, formatting and copyOutbound conflicts, keeping both Dushnilin's custom features and ushan0v's frontend performance optimizations.
- backend: Kept Dushnilin's ucode refactorings, Telegram bot integration, and release repository URL (Dushnilin/tachyon) while merging updates.uc cache format v8 & persistent cache format v7.
- tests: Merged tests from both branches, preserving ucode -L wrappers for local test execution.
2026-07-14 08:01:45 +03:00

300 lines
12 KiB
Bash

#!/usr/bin/env bash
set -eo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
UI_UC="$ROOT_DIR/tachyon/files/usr/lib/service/ui.uc"
TACHYON_FILES="$ROOT_DIR/tachyon/files"
TACHYON_BIN="$TACHYON_FILES/usr/bin/tachyon"
TACHYON_INIT="$TACHYON_FILES/etc/init.d/tachyon"
UI_RUNTIME_SH="$TACHYON_FILES/usr/lib/ui_runtime.sh"
WORK_DIR="$(mktemp -d)"
cleanup() {
rm -rf "$WORK_DIR"
}
trap cleanup EXIT
fail() {
printf 'FAIL: %s\n' "$1" >&2
exit 1
}
ui_ucode() {
ucode -L "$TACHYON_FILES/usr/lib" "$UI_UC" "$@"
}
assert_eq() {
local expected="$1"
local actual="$2"
local label="$3"
[ "$actual" = "$expected" ] || fail "$label: expected '$expected', got '$actual'"
}
write_state() {
local name="$1"
local content="$2"
printf '%s\n' "$content" >"$WORK_DIR/$name.json"
printf '%s\n' "$WORK_DIR/$name.json"
}
[ ! -e "$UI_RUNTIME_SH" ] ||
fail "ui_runtime.sh shell owner must be removed"
if grep -R -n "ui_runtime.sh" "$TACHYON_FILES" >/dev/null 2>&1; then
fail "runtime files must not reference ui_runtime.sh"
fi
ui_runtime_shell_symbols='ui_runtime_|tachyon_fast_get_ui_state|TACHYON_UI_RUNTIME|load_ui_runtime'
if grep -R -n -E "$ui_runtime_shell_symbols" "$TACHYON_BIN" "$TACHYON_INIT" "$TACHYON_FILES/usr/lib" --include='*.sh' >/dev/null 2>&1; then
fail "ui_runtime shell symbols must not remain in runtime shell"
fi
if grep -R -n '^get_ui_capabilities()' "$TACHYON_FILES/usr/lib" --include='*.sh' >/dev/null 2>&1; then
fail "UI capabilities must be owned by service/ui.uc, not shell"
fi
grep -Fq 'require("core.uci")' "$UI_UC" ||
fail "service/ui.uc must use core.uci for UI UCI-derived state"
if grep -n -E 'require\("uci"\)\.cursor|uci -q|uci", "-q"' "$UI_UC" >/dev/null 2>&1; then
fail "service/ui.uc must not own direct UCI cursor or CLI reads"
fi
grep -Fq '"tachyon-stably-running"' "$UI_UC" ||
fail "UI Tachyon status must use stable runtime state to avoid crash-loop flicker"
grep -Fq '"sing-box-service-stable"' "$UI_UC" ||
fail "UI sing-box status must use stable runtime state to avoid crash-loop flicker"
if sed -n '/^function ensure_dir(/,/^}/p' "$UI_UC" | grep -Fq 'mkdir", "-p'; then
fail "UI state refresh must not spawn mkdir for existing directories"
fi
if sed -n '/^function cleanup_dir(/,/^}/p' "$UI_UC" | grep -Fq '"find"'; then
fail "UI state refresh must clean action files without spawning find"
fi
grep -Fq 'run_pending_reload_after_service_action(action, success)' "$UI_UC" ||
fail "UI service actions must run pending reload after the current action finishes"
grep -Fq 'service_action_wait_for_expected_state(action, SERVICE_ACTION_TIMEOUT_SECONDS, SERVICE_ACTION_SETTLE_SECONDS)' "$UI_UC" ||
fail "UI service action worker must keep action state until the expected service state is stable"
grep -Fq 'let args = [ SERVICE_INIT, action ];' "$UI_UC" ||
fail "UI service action worker must use init.d so procd triggers stay registered"
grep -Fq 'start_service_action("reload", "initd", "pending")' "$UI_UC" ||
fail "pending reload must enter the normal reload service action immediately"
if grep -n -E 'pgrep.*sing-box|service-list-instance-running' "$UI_UC" >/dev/null 2>&1; then
fail "service/ui.uc must not use transient sing-box process probes for visible status"
fi
for mode in \
get-ui-capabilities \
get-ui-state \
component-action-running-for \
service-action-begin-if-idle \
service-action-update-pid \
service-action-finish-after-command \
latency-progress-state \
service-action-async \
service-action-status \
latency-test-async \
latency-test-status \
action-ack; do
grep -Fq "\"$mode\"" "$UI_UC" ||
fail "service/ui.uc must expose $mode"
done
assert_eq "/tmp/ui/job-1.json" \
"$(ui_ucode job-state-path /tmp/ui job-1)" \
"valid UI job path"
if ui_ucode job-state-path /tmp/ui '../bad' >/dev/null 2>&1; then
fail "invalid UI job id should be rejected"
fi
ui_ucode service-action-valid restart >/dev/null ||
fail "restart service action should be valid"
if ui_ucode service-action-valid invalid >/dev/null 2>&1; then
fail "invalid service action should be rejected"
fi
assert_eq "1" \
"$(ui_ucode service-action-expected-running start)" \
"start expects running service"
assert_eq "1" \
"$(ui_ucode service-action-expected-running reload)" \
"reload expects running service"
assert_eq "0" \
"$(ui_ucode service-action-expected-running stop)" \
"stop expects stopped service"
if ui_ucode service-action-expected-running invalid >/dev/null 2>&1; then
fail "invalid service action expected state should be rejected"
fi
ui_ucode latency-type-valid proxy_list >/dev/null ||
fail "proxy_list latency type should be valid"
if ui_ucode latency-type-valid invalid >/dev/null 2>&1; then
fail "invalid latency type should be rejected"
fi
latency_action_dir="$WORK_DIR/latency-actions"
mkdir -p "$latency_action_dir"
latency_state="$latency_action_dir/latency-1.json"
printf '%s\n' '{"success":true,"running":true,"kind":"latency","latency_type":"proxy_list","section":"main","tag":"[]","started_at":100}' >"$latency_state"
TACHYON_UI_LATENCY_ACTION_DIR="$latency_action_dir" \
ui_ucode latency-progress-state "$latency_state" 2 5 1 >/dev/null ||
fail "latency-progress-state should update running latency jobs"
JOB_STATE="$latency_state" node - <<'NODE'
const fs = require("fs");
const value = JSON.parse(fs.readFileSync(process.env.JOB_STATE, "utf8"));
if (!value.progress || value.progress.completed !== 2 || value.progress.total !== 5 || value.progress.failed !== 1) {
console.error("latency progress state mismatch");
process.exit(1);
}
NODE
assert_eq "running & enabled" \
"$(ui_ucode service-status-text 1 1)" \
"running enabled status"
assert_eq "running but disabled" \
"$(ui_ucode service-status-text 1 0)" \
"running disabled status"
assert_eq "stopped but enabled" \
"$(ui_ucode service-status-text 0 1)" \
"stopped enabled status"
assert_eq "stopped & disabled" \
"$(ui_ucode service-status-text 0 0)" \
"stopped disabled status"
running="$(write_state running '{"running":true,"pid":"456","started_at":100}')"
assert_eq "$(printf 'pid\t456\t0')" \
"$(ui_ucode job-refresh-plan "$running" 105 15)" \
"running UI job within grace"
assert_eq "$(printf 'pid\t456\t1')" \
"$(ui_ucode job-refresh-plan "$running" 120 15)" \
"running UI job after grace"
invalid_pid="$(write_state invalid-pid '{"running":true,"pid":"","started_at":100}')"
assert_eq "skip" \
"$(ui_ucode job-refresh-plan "$invalid_pid" 105 15)" \
"invalid UI pid within grace"
assert_eq "stale" \
"$(ui_ucode job-refresh-plan "$invalid_pid" 120 15)" \
"invalid UI pid after grace"
finished="$(write_state finished '{"running":false,"pid":"456","started_at":100}')"
assert_eq "skip" \
"$(ui_ucode job-refresh-plan "$finished" 120 15)" \
"finished UI job refresh"
stale_json="$(ui_ucode stale-action-state "$invalid_pid" "worker exited" 200)"
JSON_VALUE="$stale_json" node - <<'NODE'
const value = JSON.parse(process.env.JSON_VALUE);
if (value.running !== false || value.success !== false || value.message !== "worker exited") {
console.error("UI stale state shape mismatch");
process.exit(1);
}
NODE
acked="$(write_state acked '{"running":false,"acked_at":100}')"
ui_ucode action-ack-expired "$acked" 200 90 >/dev/null ||
fail "acked state should be expired"
if ui_ucode action-ack-expired "$acked" 150 90 >/dev/null 2>&1; then
fail "acked state should not expire before TTL"
fi
bad_ack="$(write_state bad-ack '{"running":false,"acked_at":"invalid"}')"
if ui_ucode action-ack-expired "$bad_ack" 200 90 >/dev/null 2>&1; then
fail "invalid ack timestamp should not expire"
fi
cleanup_dir="$WORK_DIR/cleanup-actions"
mkdir -p "$cleanup_dir"
printf '%s\n' '{"running":false,"acked_at":1}' >"$cleanup_dir/done.json"
printf 'stdout\n' >"$cleanup_dir/done.out"
printf '{}\n' >"$cleanup_dir/done.out.json"
ui_ucode cleanup-action-dir-fixture "$cleanup_dir"
[ ! -e "$cleanup_dir/done.json" ] ||
fail "expired acknowledged action state should be cleaned"
[ ! -e "$cleanup_dir/done.out" ] ||
fail "expired acknowledged action stdout sidecar should be cleaned"
[ ! -e "$cleanup_dir/done.out.json" ] ||
fail "expired acknowledged action JSON sidecar should be cleaned"
old_state="$cleanup_dir/old.json"
printf '%s\n' '{"running":false}' >"$old_state"
touch -d '2 hours ago' "$old_state"
TACHYON_UI_ACTION_FINISHED_TTL_MINUTES=60 ui_ucode cleanup-action-dir-fixture "$cleanup_dir"
[ ! -e "$old_state" ] ||
fail "old finished action state should be cleaned without find"
export TACHYON_UI_STATE_DIR="$WORK_DIR/ui-state"
export TACHYON_UI_SERVICE_ACTION_DIR="$TACHYON_UI_STATE_DIR/service-actions"
export TACHYON_UI_SERVICE_ACTION_LOCK_DIR="$TACHYON_UI_STATE_DIR/service-actions.lock"
export TACHYON_UI_LATENCY_ACTION_DIR="$TACHYON_UI_STATE_DIR/latency-actions"
export TACHYON_UI_COMPONENT_ACTION_DIR="$TACHYON_UI_STATE_DIR/component-actions"
export TACHYON_UI_SUBSCRIPTION_ACTION_DIR="$TACHYON_UI_STATE_DIR/subscription-actions"
latency_start="$(
TACHYON_BIN=/bin/true \
TACHYON_LIB="$TACHYON_FILES/usr/lib" \
ui_ucode latency-test-async proxy_list main '["proxy-a","proxy-b"]' 5000
)"
latency_job_id="$(printf '%s' "$latency_start" | sed -n 's/.*"job_id"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p')"
[ -n "$latency_job_id" ] || fail "latency-test-async should return a job id"
latency_async_state="$TACHYON_UI_LATENCY_ACTION_DIR/$latency_job_id.json"
[ -f "$latency_async_state" ] || fail "latency-test-async should create a state file"
JOB_STATE="$latency_async_state" node - <<'NODE'
const fs = require("fs");
const value = JSON.parse(fs.readFileSync(process.env.JOB_STATE, "utf8"));
if (!value.progress || value.progress.completed !== 0 || value.progress.total !== 2 || value.progress.failed !== 0) {
console.error("initial latency progress state mismatch");
process.exit(1);
}
NODE
job_id="$(ui_ucode service-action-begin-if-idle reload test)"
[ -n "$job_id" ] || fail "service-action-begin-if-idle should create a job"
service_state="$TACHYON_UI_SERVICE_ACTION_DIR/$job_id.json"
[ -f "$service_state" ] || fail "service action state file should be created"
assert_eq "reload" \
"$(ui_ucode active-service-action)" \
"active-service-action should use the default service action dir"
if ui_ucode service-action-begin-if-idle restart test >/dev/null 2>&1; then
fail "second service action should be rejected while first is running"
fi
ui_ucode service-action-update-pid "$job_id" "$$" >/dev/null ||
fail "service-action-update-pid should update running job"
TACHYON_UI_SERVICE_ACTION_TIMEOUT_SECONDS=1 \
TACHYON_UI_SERVICE_ACTION_SETTLE_SECONDS=0 \
TACHYON_SERVICE_INIT=/bin/true \
TACHYON_BIN=/bin/true \
TACHYON_LIB="$TACHYON_FILES/usr/lib" \
ui_ucode service-action-finish-after-command reload "$job_id" 0 >/dev/null ||
fail "service-action-finish-after-command should spawn waiter without ucode declaration-order failure"
sleep 2
if grep -q '"running"[[:space:]]*:[[:space:]]*true' "$service_state"; then
fail "service-action-finish-after-command waiter should finish the running service action"
fi
ui_ucode service-action-finish "$job_id" true done 0 >/dev/null ||
fail "service-action-finish should finish running job"
JOB_STATE="$service_state" node - <<'NODE'
const fs = require("fs");
const value = JSON.parse(fs.readFileSync(process.env.JOB_STATE, "utf8"));
if (value.running !== false || value.success !== true || value.message !== "done" || value.exit_code !== 0) {
console.error("finished service action state mismatch");
process.exit(1);
}
NODE
component_job="$TACHYON_UI_COMPONENT_ACTION_DIR/component-1.json"
mkdir -p "$TACHYON_UI_COMPONENT_ACTION_DIR"
printf '%s\n' '{"running":true,"component":"sing_box","started_at":100}' >"$component_job"
ui_ucode component-action-running-for sing_box >/dev/null ||
fail "component-action-running-for should detect running sing-box component action"
if ui_ucode component-action-running-for zapret >/dev/null 2>&1; then
fail "component-action-running-for should ignore other components"
fi
ui_ucode action-ack service "$job_id" >/dev/null ||
fail "action-ack should acknowledge finished service action"
printf 'UI runtime job checks passed\n'