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).
111 lines
3.9 KiB
Go
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)
|
|
}
|
|
}
|