CI / go-test (push) Waiting to run
CI / postgres-durable-first (push) Waiting to run
CI / codegen (push) Waiting to run
CI / govulncheck (push) Waiting to run
CI / race (push) Waiting to run
CI / fuzz-smoke (push) Waiting to run
CI / golangci (push) Waiting to run
CI / frontend (push) Waiting to run
Docs CI / build (push) Waiting to run
Docs Deploy (GitHub Pages) / build (push) Waiting to run
Docs Deploy (GitHub Pages) / deploy (push) Blocked by required conditions
Release 3X-UI / build (386) (push) Waiting to run
Release 3X-UI / build (amd64) (push) Waiting to run
Release 3X-UI / build (arm64) (push) Waiting to run
Release 3X-UI / build (armv5) (push) Waiting to run
Release 3X-UI / build (armv6) (push) Waiting to run
Release 3X-UI / build (armv7) (push) Waiting to run
Release 3X-UI / build (s390x) (push) Waiting to run
Release 3X-UI / Build for Windows (amd64) (push) Waiting to run
Release 3X-UI / Publish rolling dev release (push) Blocked by required conditions
CodeQL Advanced / Analyze (go) (push) Waiting to run
CodeQL Advanced / Analyze (actions) (push) Waiting to run
CodeQL Advanced / Analyze (javascript-typescript) (push) Waiting to run
- 0: sing-box gen overrides fragment listen/port from the inbound row (empty listen -> "::"), Update/Add recompute the stored fragment; non-loopback E2E against the real core binary. - 1: agent fetches the sing-box core from the Gitea release when missing, self-checks it, and only then latches the switch. - 2: node list shows the sing-box core version (heartbeat field, persisted on the node row), xray version as fallback. - 3: public-page fallback returns 404 to anonymous visitors when the page is disabled. - 4: settings restart hint bar removed, dead i18n keys cleaned. - 5: two PUT calls missing JSON headers fixed (audit: only those two), batch probe-task creation endpoint + UI. - 6: cf-probe retirement card and coverage pipeline removed. - 7: generated vless links no longer carry spx (panel export + sub); parser still accepts spx; golden fixtures updated. - probe batch: dedicated probe settings tab (public-page settings, tasks, per-node meta), retention_days editable, per-node traffic calibration + stat-interface pinning, runtime metrics split into a lightweight fast report (default 3s, 1-60s adjustable). - 8: sing-box core processes start with GOMEMLIMIT=32MiB, GOGC=50. - 9: dead /getTwoFactorEnable docs entries removed (frontend call and contract entry already gone at base; verified by grep).
60 lines
1.7 KiB
Go
60 lines
1.7 KiB
Go
package agent
|
|
|
|
import (
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"sync/atomic"
|
|
"testing"
|
|
)
|
|
|
|
// V2.0.1 ① integration: the master serves a sing-box config but the
|
|
// node has no core and the fetch fails — the switch must NOT latch
|
|
// (xray stays the runtime and the next poll retries), because the
|
|
// B5 switch retires xray the moment it engages.
|
|
func TestSwitchGateStaysOpenWhenCoreFetchFails(t *testing.T) {
|
|
binDir := t.TempDir()
|
|
t.Setenv("XUI_BIN_FOLDER", binDir)
|
|
|
|
var hits atomic.Int32
|
|
coreSrv := fakeCoreServer(t, &hits, http.StatusBadGateway)
|
|
defer coreSrv.Close()
|
|
useCoreBase(t, coreSrv.URL)
|
|
|
|
master := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if strings.HasSuffix(r.URL.Path, "/config") {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = w.Write([]byte(`{"config_hash":"x1","singbox_config_hash":"sb1",` +
|
|
`"singbox_config":{"inbounds":[]}}`))
|
|
return
|
|
}
|
|
http.NotFound(w, r)
|
|
}))
|
|
defer master.Close()
|
|
|
|
a := &Agent{
|
|
config: &Config{MasterURL: master.URL, Token: "tok", NodeName: "n"},
|
|
httpClient: http.DefaultClient,
|
|
probes: newProbeRunner(),
|
|
}
|
|
if err := a.fetchAndApplyConfig(); err != nil {
|
|
t.Fatalf("fetchAndApplyConfig: %v", err)
|
|
}
|
|
if a.sbSwitched {
|
|
t.Fatal("switch latched although the sing-box core could not be fetched")
|
|
}
|
|
if hits.Load() != 1 {
|
|
t.Errorf("core fetch attempts = %d, want exactly 1", hits.Load())
|
|
}
|
|
// A second poll inside the backoff window must not re-download.
|
|
if err := a.fetchAndApplyConfig(); err != nil {
|
|
t.Fatalf("second fetchAndApplyConfig: %v", err)
|
|
}
|
|
if hits.Load() != 1 {
|
|
t.Errorf("backoff violated on the config path: hits = %d", hits.Load())
|
|
}
|
|
if a.sbSwitched {
|
|
t.Fatal("switch latched during backoff")
|
|
}
|
|
}
|