11package prices_test
22
33import (
4+ "fmt"
45 "testing"
56
67 "github.com/prometheus/client_golang/prometheus"
@@ -58,6 +59,7 @@ func TestSeedFromBytes(t *testing.T) {
5859 require .Equal (t , int64 (25_000_000 ), opus .OutputPrice .Int64 )
5960 require .Equal (t , int64 (500_000 ), opus .CacheReadPrice .Int64 )
6061 require .Equal (t , int64 (6_250_000 ), opus .CacheWritePrice .Int64 )
62+ require .Equal (t , opus .CreatedAt , opus .UpdatedAt )
6163
6264 // Spot-check a row where the seed has a NULL price (OpenAI does not
6365 // publish a cache_write_price). The column should land as SQL NULL.
@@ -90,11 +92,11 @@ func TestSeedFromBytes(t *testing.T) {
9092 })
9193 require .NoError (t , err )
9294
93- // Prices must be identical across runs and CreatedAt must be
94- // preserved (only updated_at moves on a no-op upsert).
95+ // A re-seed that changes nothing must not touch the row at all.
9596 require .Equal (t , first .InputPrice , second .InputPrice )
9697 require .Equal (t , first .OutputPrice , second .OutputPrice )
9798 require .Equal (t , first .CreatedAt , second .CreatedAt )
99+ require .Equal (t , first .UpdatedAt , second .UpdatedAt )
98100 })
99101
100102 t .Run ("OverwritesExistingPrices" , func (t * testing.T ) {
@@ -114,6 +116,10 @@ func TestSeedFromBytes(t *testing.T) {
114116 "cache_read_price": 3,
115117 "cache_write_price": 4
116118 }]` )))
119+ before , err := db .GetAIModelPriceByProviderModel (ctx , database.GetAIModelPriceByProviderModelParams {
120+ Provider : "openai" , Model : "gpt-4o" ,
121+ })
122+ require .NoError (t , err )
117123
118124 require .NoError (t , prices .SeedFromBytes (ctx , db , []byte (testSeedJSON )))
119125
@@ -126,6 +132,8 @@ func TestSeedFromBytes(t *testing.T) {
126132 require .Equal (t , int64 (1_250_000 ), got .CacheReadPrice .Int64 )
127133 require .False (t , got .CacheWritePrice .Valid )
128134 require .Zero (t , got .CacheWritePrice .Int64 )
135+ require .Equal (t , before .CreatedAt , got .CreatedAt )
136+ require .True (t , got .UpdatedAt .After (before .UpdatedAt ))
129137 })
130138
131139 t .Run ("LeavesOrphanRowsUntouched" , func (t * testing.T ) {
@@ -174,6 +182,101 @@ func TestSeedFromBytes(t *testing.T) {
174182 require .True (t , got .InputPrice .Valid )
175183 require .Equal (t , int64 (2_500_000 ), got .InputPrice .Int64 )
176184 })
185+
186+ // Every price column counts toward the comparison, and a NULL on either
187+ // side counts as a difference.
188+ t .Run ("UpdatedAtTracksPriceChanges" , func (t * testing.T ) {
189+ t .Parallel ()
190+
191+ key := database.GetAIModelPriceByProviderModelParams {Provider : "openai" , Model : "gpt-4o" }
192+ seed := func (priceFields string ) []byte {
193+ return fmt .Appendf (nil , `[{"provider": %q, "model": %q, %s}]` , key .Provider , key .Model , priceFields )
194+ }
195+
196+ tests := []struct {
197+ name string
198+ initial , updated string
199+ }{
200+ {
201+ name : "InputPriceChanged" ,
202+ initial : `"input_price": 100, "output_price": 200, "cache_read_price": 300, "cache_write_price": 400` ,
203+ updated : `"input_price": 111, "output_price": 200, "cache_read_price": 300, "cache_write_price": 400` ,
204+ },
205+ {
206+ name : "InputPriceSetFromNull" ,
207+ initial : `"input_price": null, "output_price": 200, "cache_read_price": 300, "cache_write_price": 400` ,
208+ updated : `"input_price": 100, "output_price": 200, "cache_read_price": 300, "cache_write_price": 400` ,
209+ },
210+ {
211+ name : "InputPriceClearedToNull" ,
212+ initial : `"input_price": 100, "output_price": 200, "cache_read_price": 300, "cache_write_price": 400` ,
213+ updated : `"input_price": null, "output_price": 200, "cache_read_price": 300, "cache_write_price": 400` ,
214+ },
215+ {
216+ name : "OutputPriceChanged" ,
217+ initial : `"input_price": 100, "output_price": 200, "cache_read_price": 300, "cache_write_price": 400` ,
218+ updated : `"input_price": 100, "output_price": 222, "cache_read_price": 300, "cache_write_price": 400` ,
219+ },
220+ {
221+ name : "OutputPriceSetFromNull" ,
222+ initial : `"input_price": 100, "output_price": null, "cache_read_price": 300, "cache_write_price": 400` ,
223+ updated : `"input_price": 100, "output_price": 200, "cache_read_price": 300, "cache_write_price": 400` ,
224+ },
225+ {
226+ name : "OutputPriceClearedToNull" ,
227+ initial : `"input_price": 100, "output_price": 200, "cache_read_price": 300, "cache_write_price": 400` ,
228+ updated : `"input_price": 100, "output_price": null, "cache_read_price": 300, "cache_write_price": 400` ,
229+ },
230+ {
231+ name : "CacheReadPriceChanged" ,
232+ initial : `"input_price": 100, "output_price": 200, "cache_read_price": 300, "cache_write_price": 400` ,
233+ updated : `"input_price": 100, "output_price": 200, "cache_read_price": 333, "cache_write_price": 400` ,
234+ },
235+ {
236+ name : "CacheReadPriceSetFromNull" ,
237+ initial : `"input_price": 100, "output_price": 200, "cache_read_price": null, "cache_write_price": 400` ,
238+ updated : `"input_price": 100, "output_price": 200, "cache_read_price": 300, "cache_write_price": 400` ,
239+ },
240+ {
241+ name : "CacheReadPriceClearedToNull" ,
242+ initial : `"input_price": 100, "output_price": 200, "cache_read_price": 300, "cache_write_price": 400` ,
243+ updated : `"input_price": 100, "output_price": 200, "cache_read_price": null, "cache_write_price": 400` ,
244+ },
245+ {
246+ name : "CacheWritePriceChanged" ,
247+ initial : `"input_price": 100, "output_price": 200, "cache_read_price": 300, "cache_write_price": 400` ,
248+ updated : `"input_price": 100, "output_price": 200, "cache_read_price": 300, "cache_write_price": 444` ,
249+ },
250+ {
251+ name : "CacheWritePriceSetFromNull" ,
252+ initial : `"input_price": 100, "output_price": 200, "cache_read_price": 300, "cache_write_price": null` ,
253+ updated : `"input_price": 100, "output_price": 200, "cache_read_price": 300, "cache_write_price": 400` ,
254+ },
255+ {
256+ name : "CacheWritePriceClearedToNull" ,
257+ initial : `"input_price": 100, "output_price": 200, "cache_read_price": 300, "cache_write_price": 400` ,
258+ updated : `"input_price": 100, "output_price": 200, "cache_read_price": 300, "cache_write_price": null` ,
259+ },
260+ }
261+
262+ for _ , tt := range tests {
263+ t .Run (tt .name , func (t * testing.T ) {
264+ t .Parallel ()
265+ ctx := testutil .Context (t , testutil .WaitShort )
266+ db , _ := dbtestutil .NewDB (t )
267+
268+ require .NoError (t , prices .SeedFromBytes (ctx , db , seed (tt .initial )))
269+ before , err := db .GetAIModelPriceByProviderModel (ctx , key )
270+ require .NoError (t , err )
271+
272+ require .NoError (t , prices .SeedFromBytes (ctx , db , seed (tt .updated )))
273+ after , err := db .GetAIModelPriceByProviderModel (ctx , key )
274+ require .NoError (t , err )
275+
276+ require .True (t , after .UpdatedAt .After (before .UpdatedAt ), "updated_at should advance when a price changes" )
277+ })
278+ }
279+ })
177280}
178281
179282// TestSeed exercises the real embedded prices.json so we catch a corrupted,
0 commit comments