🐤 Sync 2026-10-03 00:12:36

This commit is contained in:
github-actions[bot] committed 2026-10-03 00:12:36 +08:00
1 parent ce3df55b24
commit cb1d978497
58 files changed
+1627 -1018

No files matched your search

+82 -13
View File
@@ -1009,9 +1009,25 @@ fi
# No shipped file may cite docs/. The whole directory is gitignored, so every
# pointer to it is a dead link in a fresh clone - that is how README.md
# came to reference an agent guide and an improvement plan that no
# contributor can open. Kept here (with the docs/ pattern, not the names)
# so the rule cannot quietly come back.
DEAD_DOCS="$(grep -rlE 'docs/(adr|architecture-improvement-plan|homeproxy_architecture_refactor_agent_guide|audit-report|next-step-plan)' \
# contributor can open.
#
# The pattern is deliberately *shape*-based, not a list of names. An earlier
# version enumerated the offending documents
# (adr|architecture-improvement-plan|homeproxy_architecture_refactor_agent_guide|
# audit-report|next-step-plan) and therefore only ever caught those five: the
# next document to be written - the gap analysis, the dated review reports -
# walked straight past it, and a dozen comments across the generators, the
# views and the test suite kept citing
# `docs/linux.json 与 pro 的差距分析.md` with no gate noticing.
# Matching `docs/` followed by anything catches every present and future name.
#
# The anchor is what keeps it from crying wolf. `htdocs/` contains the
# substring `docs/`, and so does the `/docs/` of an external URL such as
# https://github.com/anytls/anytls-go/blob/v0.0.8/docs/uri_scheme.md - both
# are legitimate and must not be flagged. Requiring the character before
# `docs/` to be neither alphanumeric nor `_`, `.`, `/`, `-` or `:` excludes
# exactly those two shapes and keeps every real citation.
DEAD_DOCS="$(grep -rlE '(^|[^:A-Za-z0-9_./-])docs/[^[:space:]]' \
"$ROOT/README.md" "$ROOT/root" "$ROOT/htdocs" 2>/dev/null || true)"
if [ -z "$DEAD_DOCS" ]; then
pass "no shipped file cites the gitignored docs/ tree"
@@ -1020,6 +1036,18 @@ else
printf '%s\n' "$DEAD_DOCS"
fi
# The same rule for the test tree, minus this file: a dead citation inside a
# test is just as unresolvable for a contributor reading the failure, and the
# generator suite carried six of them.
DEAD_DOCS_TESTS="$(grep -rlE '(^|[^:A-Za-z0-9_./-])docs/[^[:space:]]' \
"$ROOT/tests" 2>/dev/null | grep -v 'tests/arch-guard\.sh$' || true)"
if [ -z "$DEAD_DOCS_TESTS" ]; then
pass "no test file cites the gitignored docs/ tree either"
else
fail "these test files cite docs/ (gitignored, absent from every clone):"
printf '%s\n' "$DEAD_DOCS_TESTS"
fi
echo
echo "== guard 21: dnsmasq snippet writer is incremental =="
@@ -1716,9 +1744,8 @@ echo "== guard 38: append_custom_dns emits the HTTPS/SVCB reject as its first DN
# The proxy path emits this reject (append_proxy_dns line 102); the custom
# path used to rely on whatever the user wrote, so a rule that targets the
# same query_type could shadow the safety net. The fix (see
# docs/linux.json 与 pro 的差距分析.md §2.2) prepends the same literal in
# append_custom_dns. Two checks keep that ordering:
# same query_type could shadow the safety net. The fix prepends the same
# literal in append_custom_dns. Two checks keep that ordering:
#
# (a) the literal appears in BOTH append_proxy_dns and append_custom_dns;
# (b) the second occurrence (the one inside append_custom_dns) comes
@@ -1732,7 +1759,7 @@ if [ "$reject_count" -eq 2 ]; then
pass "the HTTPS/SVCB reject appears in both append_proxy_dns and append_custom_dns"
else
fail "expected 2 occurrences of 'query_type: [64, 65]' in generator/dns.uc, found $reject_count"
fail "the custom path lost its built-in reject; see §2.2 of the gap analysis"
fail "the custom path lost its built-in reject (see guard 38 in this file)"
fi
# (b): the custom-path reject is the SECOND occurrence (append_custom_dns
@@ -1759,9 +1786,7 @@ fi
echo "== guard 39: sniffer_advanced_mode default stays at '0' =="
# §2.7 (linux.json 与 pro 的差距分析.md): the advanced-mode sniff profile
# §2.7 (linux.json 与 pro 的差距分析.md): the advanced-mode sniff profile
# The advanced-mode sniff profile
# (100ms / universal list) is gated behind a UCI opt-in so an upgrade
# does not change the sniffer behaviour. The default must remain '0';
# the generator falls back to '0' on a missing UCI value, and the
@@ -1869,7 +1894,7 @@ fi
echo "== guard 41: experimental.cache_file is unconditional in attachExperimental() =="
# Review (2026-09-29, docs/review-report-20260929.md §1.4.3 / §5.1, P2 #3):
# The 2026-09-29 review (P2 #3) found:
# attachExperimental() used to wrap the experimental.cache_file block in
# `if (routing_mode in ['bypass_mainland_china', 'custom'])`, leaving
# gfwlist / proxy_mainland_china / global with cold-start DNS on every
@@ -1921,7 +1946,7 @@ esac
echo "== guard 42: NAPTR_BYPASS_SUFFIXES is the only domain_suffix qtype-35 source =="
# Review (2026-09-29, docs/review-report-20260929.md §2.2.1, P2 #4): the three
# The 2026-09-29 review (P2 #4) found: the three
# NAPTR (qtype 35) bypass suffixes (r.10086.cn, 10086.cn, pub.3gppnetwork.org)
# used to be inlined into a push() call inside append_proxy_dns() and the
# comment that explained them was on the same line as the push. The fix
@@ -1948,7 +1973,7 @@ fi
echo
echo "== guard 43: the rollback UCI snapshot matches the keys the intercept layer reads =="
# Review 2026-09-29 (docs/review-report-20260929-full.md §1.2-1.4, P2 #1/#2).
# The 2026-09-29 review (P2 #1 / #2) found:
#
# reload_service rolls back by restoring the known-good sing-box file *and*
# replaying a UCI snapshot of the keys that steer the network layer - the
@@ -2339,6 +2364,50 @@ else
pass "no view calls the non-existent form.Map.formvalue()"
fi
echo "== guard 49: the i18n package name cannot drift from PKG_NAME =="
# luci.mk names the translation package from the *checkout directory*, not from
# PKG_NAME:
#
# LUCI_NAME?=$(notdir ${CURDIR})
# LUCI_BASENAME?=$(patsubst luci-$(LUCI_TYPE)-%,%,$(LUCI_NAME))
# define Package/luci-i18n-$(LUCI_BASENAME)-$(1)
#
# PKG_NAME is pinned to luci-app-homeproxy-pro here, so with the repository cloned
# under its own name (luci-app-homeproxy-pro, which is what every self-compiler
# gets) the feed build emitted luci-i18n-homeproxy-pro-zh-cn while the published
# apk was named by build-pkg.sh from PKG_NAME and came out
# luci-i18n-homeproxy-pro-zh-cn. Two names, one project, and the person self-building
# concludes the translation package is missing (issue #5).
#
# The check is that LUCI_BASENAME agrees with the PKG_NAME the release path uses,
# computed here from the Makefile the way luci.mk would - not by re-deriving it
# from this repository's own directory name, which is precisely the thing that
# was wrong and would make the assertion vacuous.
MAKEFILE="$ROOT/Makefile"
MK_PKG_NAME="$(sed -n 's/^PKG_NAME:=//p' "$MAKEFILE" 2>"/dev/null" | head -1)"
MK_LUCI_BASENAME="$(sed -n 's/^LUCI_BASENAME:=//p' "$MAKEFILE" 2>"/dev/null" | head -1)"
if [ -z "$MK_PKG_NAME" ]; then
fail "PKG_NAME is not declared in the Makefile - nothing to keep the i18n name tied to"
elif [ -z "$MK_LUCI_BASENAME" ]; then
fail "LUCI_BASENAME is not pinned, so the i18n package name follows the checkout"
fail " directory: cloning as luci-app-homeproxy-pro yields luci-i18n-homeproxy-pro-zh-cn"
fail " while the release path names it luci-i18n-homeproxy-pro-zh-cn (issue #5)"
else
# luci.mk strips the "luci-<type>-" prefix; type is the 2nd dash-field.
mk_type="$(printf '%s' "$MK_PKG_NAME" | awk -F- '{print $2}')"
mk_expected_basename="$(printf '%s' "$MK_PKG_NAME" | sed -n "s/^luci-${mk_type}-//p")"
if [ "$MK_LUCI_BASENAME" = "$mk_expected_basename" ]; then
pass "LUCI_BASENAME matches PKG_NAME, so the i18n package is named the same"
pass " in the feed build and in the release (luci-i18n-${MK_LUCI_BASENAME}-zh-cn)"
else
fail "LUCI_BASENAME is '$MK_LUCI_BASENAME' but PKG_NAME '$MK_PKG_NAME' implies"
fail " '$mk_expected_basename' - the self-built i18n package will not match the release"
fi
fi
echo
printf '%s checks, %s failures\n' "$checks" "$([ "$FAILED" = 0 ] && echo 0 || echo 'nonzero')"
if [ "$FAILED" != 0 ]; then
@@ -133,7 +133,7 @@ expect "uci restore: missing snapshot returns 1" "$?" "1"
# Happy path: snapshot present, all keys applied, commit called.
# The snapshot format carries the UCI type, because `uci set` on a list option
# stores a string and the firewall validators then reject it (review
# 2026-09-29, docs/review-report-20260929-full.md §1.5): lists are replayed
# 2026-09-29 review): lists are replayed
# with `uci delete` + `uci add_list`.
: > "$HP_TEST_UCI_OUT"
write_uci_stub
@@ -578,7 +578,7 @@ fi
run_case custom "$ROOT/tests/fixtures/generators/custom.uci" generate_client.uc sing-box-c.json
# §2.2 (linux.json 与 pro 的差距分析): append_custom_dns now prepends the
# append_custom_dns now prepends the
# HTTPS/SVCB reject so a user-written rule cannot shadow the safety net.
# Without a dns_server in custom.uci the rule block is empty, so the
# generated config still has the reject at index 0.
@@ -613,7 +613,7 @@ EOF
fi
fi
# §2.5 (linux.json 与 pro 的差距分析): the original plan was to set
# The original plan was to set
# `experimental.reverse_mapping: true` so the literal pinned us against a
# future sing-box default flip. sing-box 1.14.0-r1 (the CI floor pinned
# by arch-guard 31) rejects the field as "json: unknown field" - the
@@ -623,7 +623,7 @@ fi
# purpose: the only assertion that matters is that the generated config
# passes sing-box check, which the test runner does for every case.
# §2.4 (linux.json 与 pro 的差距分析): when the user configures a DoH
# When the user configures a DoH
# upstream against a known hostname, append_custom_dns emits a hosts-type
# DNS server pinning that hostname to its real IPs. Without it, the DoH
# resolver itself has to be resolved through the system DNS, and a DNS
@@ -676,7 +676,7 @@ EOF
fi
fi
# §2.1 (linux.json 与 pro 的差距分析): cn_ip_fallback default flipped to '1'
# cn_ip_fallback default flipped to '1'
# for fresh installs (the package ships option cn_ip_fallback '1' and
# context.uc falls back to '1' on a missing value). Existing users
# upgrading get '0' written by migrate_config.uc and keep the prior
@@ -719,7 +719,8 @@ EOF
fi
fi
# §2.1 opt-out: a fixture with cn_ip_fallback='0' must NOT emit the pair.
# cn_ip_fallback opt-out: a fixture with cn_ip_fallback='0' must NOT emit
# the pair.
# client.uci does not set the option (so it defaults to '1' in the proxy
# config above); here we stage a variant with '0' to assert the
# upgrade-safe path of migrate_config.uc - existing users see no change.
@@ -740,7 +741,7 @@ else
fi
fi
# §2.7 (linux.json 与 pro 的差距分析): sniffer_advanced_mode default stays
# sniffer_advanced_mode default stays
# '0'. Client mode must emit the conservative sniff rule (300ms / no
# sniffer list); advanced mode emits the 100ms / universal-list profile.
if [ ! -f "$client_json" ]; then
@@ -776,7 +777,7 @@ EOF
fi
fi
# §2.7 opt-in: sniffer_advanced_mode='1' flips the sniff rule to 100ms +
# sniffer_advanced_mode opt-in: '1' flips the sniff rule to 100ms +
# the universal protocol list. sed-injected on client.uci.
run_case client-sniffer-advanced "$ROOT/tests/fixtures/generators/client.uci" generate_client.uc sing-box-c.json \
"s#^\\([[:space:]]*\\)option main_node 'urltest'#\\1option main_node 'urltest'\\n\\1option sniffer_advanced_mode '1'#"
@@ -920,8 +921,8 @@ run_case_type_error dangling-resolver "does not exist or is disabled" \
# --- review@2026-09-29 batch -------------------------------------------------
#
# Six checks that pin the five generator fixes from
# docs/review-report-20260929.md §7. Earlier rounds attempted ucode
# Six checks that pin the five generator fixes from the 2026-09-29 review.
# Earlier rounds attempted ucode
# probes (arrow functions, optional chaining on arrays, ...) and each
# round surfaced a different ucode dialect issue on the testbed - the
# macOS dev box has no ucode so the loop could not iterate locally.
@@ -929,7 +930,7 @@ run_case_type_error dangling-resolver "does not exist or is disabled" \
# it runs identically on macOS and the testbed. ucode-stdout probes
# are out; structural assertions against the emitted bytes are in.
# 1) P2 #2 / §1.4.1: geosite-cn is the *DNS*-layer rule_set consumer.
# 1) P2 #2: geosite-cn is the *DNS*-layer rule_set consumer.
# The route layer never references it as a rule_set value (route.rule_set
# declares it as a tag). Indent cannot disambiguate dns.rules[] from
# route.rules[] (both pretty-print at 4 tabs on the testbed; both are
@@ -962,7 +963,7 @@ else
fi
fi
# 2) P3 #6 / §1.4.3: china-dns.strategy must mirror default-dns's gate
# 2) P3 #6: china-dns.strategy must mirror default-dns's gate
# on ipv6_support. With ipv6_support='0' (client.uci's value),
# china-dns.strategy must be 'ipv4_only', not the pre-r28
# unconditional 'prefer_ipv6'. The generated JSON emits the dns
@@ -1083,7 +1084,7 @@ else
fi
fi
# 3) P2 #4 / §2.2.1: the three NAPTR (qtype 35) bypass suffixes must be
# 3) P2 #4: the three NAPTR (qtype 35) bypass suffixes must be
# emitted as a single domain_suffix entry backed by the
# NAPTR_BYPASS_SUFFIXES constant. ucode's pretty-printer emits the
# query_type array and the domain_suffix array as multi-line:
@@ -1121,7 +1122,7 @@ else
fi
fi
# 4) P2 #3 / §1.4.3 / §5.1: attachExperimental() used to gate cache_file
# 4) P2 #3: attachExperimental() used to gate cache_file
# on routing_mode in [bypass_mainland_china, custom]. After the r28
# fix, every routing_mode gets a cache_file block. Three sed
# variations of the same fixture cover gfwlist / proxy_mainland_china
@@ -1170,7 +1171,7 @@ else
echo "PASS: cache-file-custom: experimental.cache_file emitted for routing_mode='custom'"
fi
# 5) P3 #7 / §4.2.1: a local ruleset whose path is outside the homeproxy-pro
# 5) P3 #7: a local ruleset whose path is outside the homeproxy-pro
# whitelist (e.g. /etc/passwd) used to silently set the field to
# null. The fix is to die() with a message naming the offender.
# run_case_type_error observes the refusal. The variation targets
@@ -1181,7 +1182,7 @@ run_case_type_error local-ruleset-bad-path "outside the homeproxy-pro whitelist"
"$ROOT/tests/fixtures/generators/custom.uci" generate_client.uc sing-box-c.json \
"s%^[[:space:]]*option path '/tmp/homeproxy_test_ruleset\\.[^/]*/local-ruleset-bad-path/test.srs'% option path '/etc/passwd'%"
# 6) P3 #8 / §4.2.2 (extra_tags die() on missing {tag}): deferred to
# 6) P3 #8 (extra_tags die() on missing {tag}): deferred to
# a dedicated 'testbed-dialect' sprint. The multi-line sed to
# attach an extra_tags list onto the existing rs_local ruleset
# drifted at the first whitespace change on the testbed, and
@@ -59,7 +59,7 @@ expect('server.insecure forced null', serverInsecure.insecure, null);
/* 4. cert_path whitelist - the validator accepts /etc/homeproxy-pro/... and
* /tmp/homeproxy_..., and rejects everything else (including
* /etc/passwd and relative paths). This is the security patch
* that backs the §4 path-whitelist entry; a regression here would
* that backs the path-whitelist entry above; a regression here would
* let UCI drive sing-box into reading arbitrary files as root. */
expect('cert.path /etc/homeproxy-pro', validateHomeProxyPath('/etc/homeproxy-pro/certs/server_publickey.pem'), true);
expect('cert.path /tmp/homeproxy_', validateHomeProxyPath('/tmp/homeproxy_test/foo.pem'), true);