Files
3x-ui-agent/internal/web/service/probe/meta_v201_test.go
T
Muse 93c1620c15 v221 ⑥ probe traffic: binary GB convention + one-shot calibration
Unit unification: GB entries converted at 1e9 while every byte
formatter is binary (1024^3), so a 3000 GB limit rendered 2.73 TB
instead of 2.93 TB. calBytes and the form's limit conversion now
use 1024^3. Stored traffic_limits (bytes, typed x1e9) are rescaled
x1.073741824 exactly once at store open, guarded by the
probe_setting mark gb_1024_migrated_v1; calibration values are
stored in GB (no rescale — only their conversion changed) and
price/sort/reset-day are unitless.

One-shot calibration: the form's cal fields are deltas to apply
now. probe.MergeCalDelta folds a non-zero submitted value into the
stored standing correction and keeps the standing value on a
zero/absent submission; putMeta merges against the stored row
(SaveMeta/UpsertMeta keep plain replace — the notify sweep relies
on it). GET no longer echoes the standing correction into the form,
and the frontend keeps the cal inputs as local state that starts
empty and clears on successful save, so a later unrelated save
resubmits nothing.

Tests: calBytes(3000)=3000x2^30; migration 3.0e12 -> 3000x2^30 with
no second rescale on reopen; one-shot flow (save delta, save
unrelated with zero cal -> calibrated traffic unmoved, second delta
accumulates once). Existing TestTrafficCalibrationAppliedAndFloored
expectation updated to the binary figure. No vitest covers
ProbeMetaSection (disclosed in the report).
2026-10-23 15:02:11 +00:00

111 lines
3.9 KiB
Go

package probe
import (
"database/sql"
"path/filepath"
"testing"
"time"
)
// V2.0.1 probe batch, storage side: the calibration columns and the
// counted-interface pin round-trip through probe_meta, pre-V2.0.1
// databases gain the columns on open, and the traffic reads apply
// the calibration (floored at zero) on every figure.
func TestMetaCalibrationAndIfaceRoundTrip(t *testing.T) {
st := newTestStore(t)
meta := DefaultMeta(7)
meta.TrafficCalUpGB = 2.5
meta.TrafficCalDownGB = -1.25
meta.StatIface = "eth0"
if err := st.UpsertMeta(meta); err != nil {
t.Fatalf("upsert: %v", err)
}
back, found, err := st.Meta(7)
if err != nil || !found {
t.Fatalf("meta read: found=%v err=%v", found, err)
}
if back.TrafficCalUpGB != 2.5 || back.TrafficCalDownGB != -1.25 || back.StatIface != "eth0" {
t.Errorf("round trip = up %v down %v iface %q", back.TrafficCalUpGB, back.TrafficCalDownGB, back.StatIface)
}
}
func TestOpenMigratesLegacyMetaColumns(t *testing.T) {
path := filepath.Join(t.TempDir(), "probe.db")
db, err := sql.Open("sqlite3", path)
if err != nil {
t.Fatalf("open raw: %v", err)
}
// A pre-V2.0.1 probe_meta: the original 16 columns only (shape
// copied from the V2.0.0 schema in git history).
if _, err := db.Exec(`CREATE TABLE probe_meta (
agent_id INTEGER PRIMARY KEY, price REAL NOT NULL DEFAULT 0,
currency TEXT NOT NULL DEFAULT 'USD', billing_cycle TEXT NOT NULL DEFAULT 'monthly',
expires_at TEXT, traffic_limit INTEGER NOT NULL DEFAULT 0,
traffic_mode TEXT NOT NULL DEFAULT 'sum', traffic_reset_day INTEGER NOT NULL DEFAULT 1,
public INTEGER NOT NULL DEFAULT 1, sort INTEGER NOT NULL DEFAULT 0,
group_name TEXT NOT NULL DEFAULT '', public_remark TEXT NOT NULL DEFAULT '',
notify INTEGER NOT NULL DEFAULT 0, country_pin TEXT NOT NULL DEFAULT '',
ipv4_pin TEXT NOT NULL DEFAULT '', ipv6_pin TEXT NOT NULL DEFAULT '')`); err != nil {
t.Fatalf("create legacy table: %v", err)
}
if _, err := db.Exec(`INSERT INTO probe_meta (agent_id, group_name) VALUES (3, 'legacy')`); err != nil {
t.Fatalf("seed legacy row: %v", err)
}
_ = db.Close()
st, err := Open(path)
if err != nil {
t.Fatalf("open legacy db: %v", err)
}
defer st.Close()
back, found, err := st.Meta(3)
if err != nil || !found {
t.Fatalf("meta read: found=%v err=%v", found, err)
}
if back.GroupName != "legacy" || back.TrafficCalUpGB != 0 || back.TrafficCalDownGB != 0 || back.StatIface != "" {
t.Errorf("legacy row after migration = %+v", back)
}
back.StatIface = "ens3"
if err := st.UpsertMeta(back); err != nil {
t.Fatalf("upsert after migration: %v", err)
}
}
func TestTrafficCalibrationAppliedAndFloored(t *testing.T) {
st := newTestStore(t)
now := time.Now()
// Book 3 GB down / 1 GB up of counted traffic (baseline, delta).
book(t, st, 5, "k1/fp", 0, 0, now, nil, 0)
book(t, st, 5, "k1/fp", 3_000_000_000, 1_000_000_000, now.Add(time.Minute), nil, now.Unix())
meta := DefaultMeta(5)
meta.TrafficCalUpGB = -2 // correction exceeds the counted upload
meta.TrafficCalDownGB = 0.5 // half a GB the counters missed
if err := st.UpsertMeta(meta); err != nil {
t.Fatalf("upsert: %v", err)
}
all, err := st.AllTraffic()
if err != nil {
t.Fatalf("AllTraffic: %v", err)
}
tr := all[5]
// V2.2.1 ⑥: calibration converts on the binary convention —
// +0.5 GB = 0.5 x 2^30 = 536870912 B (was 5e8 at the old 1e9).
if tr.TotalRx != 3_536_870_912 || tr.MonthRx != 3_536_870_912 || tr.DayRx != 3_536_870_912 {
t.Errorf("rx figures after +0.5 GB calibration = %d/%d/%d, want 3536870912 each", tr.TotalRx, tr.MonthRx, tr.DayRx)
}
if tr.TotalTx != 0 || tr.MonthTx != 0 || tr.DayTx != 0 {
t.Errorf("tx figures after -2 GB calibration = %d/%d/%d, want 0 (floored)", tr.TotalTx, tr.MonthTx, tr.DayTx)
}
one, found, err := st.TrafficFor(5)
if err != nil || !found {
t.Fatalf("TrafficFor: found=%v err=%v", found, err)
}
if one.TotalRx != tr.TotalRx || one.TotalTx != tr.TotalTx {
t.Errorf("TrafficFor disagrees with AllTraffic: %+v vs %+v", one, tr)
}
}