carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package usage
2
3import (
4 "context"
5 "encoding/json"
6 "testing"
7 "time"
8
9 "github.com/carlosframework/platform/internal/release"
10)
11
12func TestEdgeDayKeyAndLabels(t *testing.T) {
13 if got := EdgeDayKey("box1", "2026-08-11"); got != "usage/edge/box1/days/2026-08-11.json" {
14 t.Fatalf("EdgeDayKey = %q", got)
15 }
16 at := time.Date(2026, 8, 11, 9, 4, 0, 0, time.UTC)
17 if got := DayOf(at); got != "2026-08-11" {
18 t.Fatalf("DayOf = %q", got)
19 }
20 if got := MonthOf(at); got != "2026-08" {
21 t.Fatalf("MonthOf = %q", got)
22 }
23 if got := HourLabel(at); got != "09" {
24 t.Fatalf("HourLabel = %q", got)
25 }
26}
27
28func TestUpsertEdgeDayMergesAcrossFlushes(t *testing.T) {
29 s := release.NewDirStore(t.TempDir())
30 ctx := context.Background()
31 at := time.Date(2026, 8, 11, 9, 10, 0, 0, time.UTC)
32
33 d1 := map[string]EdgeRow{
34 "a.example.com": {Account: "bab", App: "hello", Requests: 10, BytesOut: 1000},
35 }
36 if err := UpsertEdgeDay(ctx, s, "box1", at, d1); err != nil {
37 t.Fatal(err)
38 }
39 // Second flush, same hour: counts must ADD, not replace.
40 if err := UpsertEdgeDay(ctx, s, "box1", at.Add(5*time.Minute), d1); err != nil {
41 t.Fatal(err)
42 }
43 // Third flush, next hour and a second host.
44 d2 := map[string]EdgeRow{
45 "a.example.com": {Account: "bab", App: "hello", Requests: 1, BytesOut: 50},
46 "b.example.com": {Account: "ops", App: "site", Requests: 2, BytesOut: 20},
47 }
48 if err := UpsertEdgeDay(ctx, s, "box1", at.Add(time.Hour), d2); err != nil {
49 t.Fatal(err)
50 }
51
52 raw, _, err := s.Get(ctx, EdgeDayKey("box1", "2026-08-11"))
53 if err != nil {
54 t.Fatal(err)
55 }
56 var day EdgeDay
57 if err := json.Unmarshal(raw, &day); err != nil {
58 t.Fatal(err)
59 }
60 if day.V != 1 || day.Box != "box1" || day.Date != "2026-08-11" {
61 t.Fatalf("header = %+v", day)
62 }
63 h9 := day.Hours["09"]["a.example.com"]
64 if h9.Requests != 20 || h9.BytesOut != 2000 || h9.Account != "bab" || h9.App != "hello" {
65 t.Fatalf("hour 09 row = %+v", h9)
66 }
67 if day.Hours["10"]["b.example.com"].Requests != 2 {
68 t.Fatalf("hour 10 = %+v", day.Hours["10"])
69 }
70}
71
72// The hop counters are an ADDITIVE shape change (the paid-for wire rule):
73// every day object already in a production bucket predates them, so one
74// written without them must still parse, fold and round-trip — and a
75// single-region box, which never sees a hop, must keep writing objects that
76// are byte-identical to what it wrote before the fields existed.
77func TestEdgeDayHopFieldsAdditive(t *testing.T) {
78 // A day object as a pre-ruling binary wrote it: no hop keys at all.
79 const old = `{"v":1,"box":"box1","date":"2026-08-24","hours":{"09":{"a.example.com":` +
80 `{"account":"bab","app":"hello","requests":10,"bytes_out":1000}}}}`
81 var day EdgeDay
82 if err := json.Unmarshal([]byte(old), &day); err != nil {
83 t.Fatalf("a day object written before the hop fields existed must still parse: %v", err)
84 }
85 row := day.Hours["09"]["a.example.com"]
86 if row.Requests != 10 || row.BytesOut != 1000 || row.HopRequests != 0 || row.HopBytesOut != 0 {
87 t.Fatalf("row = %+v, want the old counters and zero hop counters", row)
88 }
89 // And it folds a hop-bearing delta on top without losing the old counts.
90 day.AddDeltas("09", map[string]EdgeRow{
91 "a.example.com": {Account: "bab", App: "hello", Requests: 3, BytesOut: 300, HopRequests: 2, HopBytesOut: 250},
92 })
93 row = day.Hours["09"]["a.example.com"]
94 if row.Requests != 13 || row.BytesOut != 1300 || row.HopRequests != 2 || row.HopBytesOut != 250 {
95 t.Fatalf("folded row = %+v", row)
96 }
97
98 // Round-trip: what a hop-annotating box writes, a hop-annotating box reads.
99 raw, err := json.Marshal(day)
100 if err != nil {
101 t.Fatal(err)
102 }
103 var back EdgeDay
104 if err := json.Unmarshal(raw, &back); err != nil {
105 t.Fatal(err)
106 }
107 if got := back.Hours["09"]["a.example.com"]; got != row {
108 t.Fatalf("round-tripped row = %+v, want %+v", got, row)
109 }
110
111 // omitempty, the single-region promise: a row with no hop traffic writes
112 // no hop keys, so a one-box deployment's objects do not change shape.
113 plain, err := json.Marshal(EdgeRow{Account: "bab", App: "hello", Requests: 1, BytesOut: 10})
114 if err != nil {
115 t.Fatal(err)
116 }
117 if want := `{"account":"bab","app":"hello","requests":1,"bytes_out":10}`; string(plain) != want {
118 t.Fatalf("hop-free row marshals as %s, want %s — omitempty keeps single-region objects unchanged", plain, want)
119 }
120}
121
122// The fold accumulates the hop counters across flushes exactly as it does the
123// totals — they are counters in the same hour bucket, not a snapshot.
124func TestEdgeDayHourFoldSumsHopFields(t *testing.T) {
125 var day EdgeDay
126 delta := map[string]EdgeRow{
127 "a.example.com": {Account: "bab", App: "hello", Requests: 4, BytesOut: 400, HopRequests: 1, HopBytesOut: 100},
128 }
129 day.AddDeltas("09", delta)
130 day.AddDeltas("09", delta)
131 day.AddDeltas("10", delta)
132
133 if got := day.Hours["09"]["a.example.com"]; got.Requests != 8 || got.BytesOut != 800 ||
134 got.HopRequests != 2 || got.HopBytesOut != 200 {
135 t.Fatalf("hour 09 row = %+v, want two flushes summed", got)
136 }
137 if got := day.Hours["10"]["a.example.com"]; got.HopRequests != 1 || got.HopBytesOut != 100 {
138 t.Fatalf("hour 10 row = %+v, want the third flush in its own bucket", got)
139 }
140 // The subset invariant, stated where a future reader will meet it: hop
141 // bytes live INSIDE bytes_out, so a consumer summing BytesOut has already
142 // counted them.
143 if got := day.Hours["09"]["a.example.com"]; got.HopBytesOut > got.BytesOut || got.HopRequests > got.Requests {
144 t.Fatalf("row = %+v — the hop counters are a subset of the totals, never an addition", got)
145 }
146}
147
148func TestUpsertEdgeDayEmptyDeltasIsNoop(t *testing.T) {
149 s := release.NewDirStore(t.TempDir())
150 if err := UpsertEdgeDay(context.Background(), s, "box1", time.Now().UTC(), nil); err != nil {
151 t.Fatal(err)
152 }
153 if keys, _ := s.List(context.Background(), "usage/"); len(keys) != 0 {
154 t.Fatalf("noop wrote %v", keys)
155 }
156}
157