Files
l-ui/internal/web/service/probe/meta_gb_v221_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

144 lines
4.6 KiB
Go

package probe
import (
"path/filepath"
"testing"
"time"
)
// V2.2.1 ⑥: GB entries convert on the binary convention (1024^3)
// every formatter uses, stored decimal-era limits are rescaled once,
// and calibration submissions are one-shot deltas folded into the
// standing correction — never re-applied by a later unrelated save.
func TestCalBytesBinaryConvention(t *testing.T) {
if got := calBytes(3000); got != 3000*(1<<30) {
t.Fatalf("calBytes(3000) = %d, want %d (3000 GiB)", got, 3000*(1<<30))
}
if got := calBytes(0); got != 0 {
t.Fatalf("calBytes(0) = %d, want 0", got)
}
if got := calBytes(-1.5); got != -1610612736 {
t.Fatalf("calBytes(-1.5) = %d, want -1610612736", got)
}
}
func TestGBConventionMigrationRescalesLimitsOnce(t *testing.T) {
path := filepath.Join(t.TempDir(), "probe.db")
st, err := Open(path)
if err != nil {
t.Fatalf("open: %v", err)
}
// Seed a legacy row as a pre-V2.2.1 database holds it — 3000 GB
// typed at the decimal convention = 3.0e12 stored bytes — and
// strike the migration mark so the next open sees an unmigrated
// database.
if _, err := st.db.Exec(`INSERT INTO probe_meta (agent_id, traffic_limit) VALUES (9, 3000000000000)`); err != nil {
t.Fatalf("seed legacy limit: %v", err)
}
if _, err := st.db.Exec(`DELETE FROM probe_setting WHERE key = ?`, gbConventionKey); err != nil {
t.Fatalf("strike mark: %v", err)
}
if err := st.Close(); err != nil {
t.Fatalf("close: %v", err)
}
st2, err := Open(path)
if err != nil {
t.Fatalf("reopen (migration run): %v", err)
}
m, found, err := st2.Meta(9)
if err != nil || !found {
t.Fatalf("meta after migration: found=%v err=%v", found, err)
}
// 3.0e12 x 1024^3/1e9 = 3000 x 2^30 exactly (~3.221e12).
if want := int64(3000) * (1 << 30); m.TrafficLimit != want {
t.Fatalf("migrated limit = %d, want %d", m.TrafficLimit, want)
}
if err := st2.Close(); err != nil {
t.Fatalf("close 2: %v", err)
}
st3, err := Open(path)
if err != nil {
t.Fatalf("reopen 2: %v", err)
}
defer st3.Close()
m, found, err = st3.Meta(9)
if err != nil || !found {
t.Fatalf("meta after second open: found=%v err=%v", found, err)
}
if want := int64(3000) * (1 << 30); m.TrafficLimit != want {
t.Fatalf("limit after second open = %d, want %d (no double rescale)", m.TrafficLimit, want)
}
}
func TestOneShotCalibrationSaveFlow(t *testing.T) {
st := newTestStore(t)
now := time.Now()
book(t, st, 5, "boot-a", 1000, 1000, now, nil, 0) // baseline: books 0
book(t, st, 5, "boot-a", 3000, 2000, now, nil, 0) // books rx 2000, tx 1000
// First save carries a +1 GB uplink correction delta.
submitted := DefaultMeta(5)
submitted.TrafficCalUpGB = 1
merged := MergeCalDelta(DefaultMeta(5), submitted)
if merged.TrafficCalUpGB != 1 {
t.Fatalf("standing after first fold = %v, want 1", merged.TrafficCalUpGB)
}
if err := st.UpsertMeta(merged); err != nil {
t.Fatalf("upsert 1: %v", err)
}
after1, ok, err := st.TrafficFor(5)
if err != nil || !ok {
t.Fatalf("traffic after save 1: ok=%v err=%v", ok, err)
}
if want := int64(1000) + (1 << 30); after1.TotalTx != want {
t.Fatalf("TotalTx after save 1 = %d, want %d", after1.TotalTx, want)
}
if after1.TotalRx != 2000 {
t.Fatalf("TotalRx after save 1 = %d, want 2000 (uncalibrated)", after1.TotalRx)
}
// Second save changes another field; the (cleared) form submits
// zero calibration — the calibrated figure must not move again.
stored, found, err := st.Meta(5)
if err != nil || !found {
t.Fatalf("stored meta: found=%v err=%v", found, err)
}
sub2 := DefaultMeta(5)
sub2.GroupName = "g2"
merged2 := MergeCalDelta(stored, sub2)
if merged2.TrafficCalUpGB != 1 {
t.Fatalf("standing after zero-delta save = %v, want 1 (unchanged)", merged2.TrafficCalUpGB)
}
if err := st.UpsertMeta(merged2); err != nil {
t.Fatalf("upsert 2: %v", err)
}
after2, ok, err := st.TrafficFor(5)
if err != nil || !ok {
t.Fatalf("traffic after save 2: ok=%v err=%v", ok, err)
}
if after2.TotalTx != after1.TotalTx {
t.Fatalf("TotalTx moved on an unrelated save: %d -> %d (double-applied calibration)", after1.TotalTx, after2.TotalTx)
}
// A further non-zero delta accumulates exactly once more.
sub3 := DefaultMeta(5)
sub3.TrafficCalUpGB = 0.5
merged3 := MergeCalDelta(merged2, sub3)
if merged3.TrafficCalUpGB != 1.5 {
t.Fatalf("standing after second delta = %v, want 1.5", merged3.TrafficCalUpGB)
}
if err := st.UpsertMeta(merged3); err != nil {
t.Fatalf("upsert 3: %v", err)
}
after3, _, err := st.TrafficFor(5)
if err != nil {
t.Fatalf("traffic after save 3: %v", err)
}
if want := int64(1000) + calBytes(1.5); after3.TotalTx != want {
t.Fatalf("TotalTx after save 3 = %d, want %d", after3.TotalTx, want)
}
}