11package cloudshuttle
22
33import (
4+ "bytes"
5+ "compress/gzip"
46 "context"
57 "crypto/rand"
68 "crypto/rsa"
@@ -17,30 +19,35 @@ import (
1719 "path/filepath"
1820 "strings"
1921 "testing"
22+ "time"
2023)
2124
22- func TestObjectNamePartitionsByProviderUserRepoAndRun (t * testing.T ) {
25+ func TestObjectNameUsesDatePartitionedGzipLayout (t * testing.T ) {
26+ restore := stubNow (t , time .Date (2026 , 7 , 12 , 20 , 17 , 3 , 0 , time .UTC ))
27+ defer restore ()
2328 got := ObjectName (Config {
2429 Prefix : "agent-traces/customer=test" ,
2530 Provider : "claude_code_web" ,
2631 UserID : "user-1" ,
2732 Repository : "asymptote-labs/agent-beacon" ,
2833 RunID : "cse_123" ,
2934 })
30- want := "agent-traces/customer=test/provider=claude_code_web/user_id=user-1/repo=asymptote-labs/agent-beacon/run_id= cse_123/runtime .jsonl"
35+ want := "agent-traces/customer=test/runtime/date=2026-07-12/1783887423-claude_code_web- cse_123.jsonl.gz "
3136 if got != want {
3237 t .Fatalf ("ObjectName = %q, want %q" , got , want )
3338 }
3439}
3540
3641func TestObjectNameUsesCursorCloudProvider (t * testing.T ) {
42+ restore := stubNow (t , time .Date (2026 , 7 , 12 , 20 , 17 , 3 , 0 , time .UTC ))
43+ defer restore ()
3744 got := ObjectName (Config {
3845 Prefix : "agent-traces" ,
3946 Provider : "cursor_cloud" ,
4047 UserID : "user-1" ,
4148 RunID : "manual-123" ,
4249 })
43- want := "agent-traces/provider=cursor_cloud/user_id=user-1/run_id= manual-123/runtime .jsonl"
50+ want := "agent-traces/runtime/date=2026-07-12/1783887423-cursor_cloud- manual-123.jsonl.gz "
4451 if got != want {
4552 t .Fatalf ("ObjectName = %q, want %q" , got , want )
4653 }
@@ -80,8 +87,11 @@ func TestResetFromEnvRemovesCloudRuntimeFiles(t *testing.T) {
8087}
8188
8289func TestUploadSendsJSONLToGCS (t * testing.T ) {
90+ restore := stubNow (t , time .Date (2026 , 7 , 12 , 20 , 17 , 3 , 0 , time .UTC ))
91+ defer restore ()
8392 key := mustRSAKey (t )
84- var uploadedPath , uploadedAuth , uploadedType , uploadedBody string
93+ var uploadedPath , uploadedAuth , uploadedType , uploadedEncoding string
94+ var uploadedBody []byte
8595 server := httptest .NewServer (http .HandlerFunc (func (w http.ResponseWriter , r * http.Request ) {
8696 switch r .URL .Path {
8797 case "/token" :
@@ -96,8 +106,9 @@ func TestUploadSendsJSONLToGCS(t *testing.T) {
96106 uploadedPath = r .URL .EscapedPath ()
97107 uploadedAuth = r .Header .Get ("Authorization" )
98108 uploadedType = r .Header .Get ("Content-Type" )
109+ uploadedEncoding = r .Header .Get ("Content-Encoding" )
99110 data , _ := io .ReadAll (r .Body )
100- uploadedBody = string ( data )
111+ uploadedBody = data
101112 w .WriteHeader (http .StatusOK )
102113 }
103114 }))
@@ -136,11 +147,14 @@ func TestUploadSendsJSONLToGCS(t *testing.T) {
136147 if uploadedType != contentTypeJSONL {
137148 t .Fatalf ("Content-Type = %q, want %q" , uploadedType , contentTypeJSONL )
138149 }
139- if ! strings .Contains (uploadedPath , "/bucket/prefix/provider=claude_code_web/user_id=user/run_id=run/runtime.jsonl" ) {
150+ if uploadedEncoding != contentEncoding {
151+ t .Fatalf ("Content-Encoding = %q, want %q" , uploadedEncoding , contentEncoding )
152+ }
153+ if ! strings .Contains (uploadedPath , "/bucket/prefix/runtime/date=2026-07-12/1783887423-claude_code_web-run.jsonl.gz" ) {
140154 t .Fatalf ("upload path = %q" , uploadedPath )
141155 }
142- if uploadedBody != "{\" event\" :\" ok\" }\n " {
143- t .Fatalf ("uploaded body = %q" , uploadedBody )
156+ if got := gunzipString ( t , uploadedBody ); got != "{\" event\" :\" ok\" }\n " {
157+ t .Fatalf ("uploaded body = %q" , got )
144158 }
145159}
146160
@@ -183,16 +197,20 @@ func TestConfigFromEnvDefaultsToGCSWhenS3BucketIsAlsoSet(t *testing.T) {
183197}
184198
185199func TestUploadSendsJSONLToS3 (t * testing.T ) {
186- var uploadedPath , uploadedAuth , uploadedDate , uploadedHash , uploadedToken , uploadedType , uploadedBody string
200+ restore := stubNow (t , time .Date (2026 , 7 , 12 , 20 , 17 , 3 , 0 , time .UTC ))
201+ defer restore ()
202+ var uploadedPath , uploadedAuth , uploadedDate , uploadedHash , uploadedToken , uploadedType , uploadedEncoding string
203+ var uploadedBody []byte
187204 server := httptest .NewServer (http .HandlerFunc (func (w http.ResponseWriter , r * http.Request ) {
188205 uploadedPath = r .URL .EscapedPath ()
189206 uploadedAuth = r .Header .Get ("Authorization" )
190207 uploadedDate = r .Header .Get ("X-Amz-Date" )
191208 uploadedHash = r .Header .Get ("X-Amz-Content-Sha256" )
192209 uploadedToken = r .Header .Get ("X-Amz-Security-Token" )
193210 uploadedType = r .Header .Get ("Content-Type" )
211+ uploadedEncoding = r .Header .Get ("Content-Encoding" )
194212 data , _ := io .ReadAll (r .Body )
195- uploadedBody = string ( data )
213+ uploadedBody = data
196214 w .WriteHeader (http .StatusOK )
197215 }))
198216 defer server .Close ()
@@ -220,21 +238,21 @@ func TestUploadSendsJSONLToS3(t *testing.T) {
220238 if err := Upload (context .Background (), cfg , true ); err != nil {
221239 t .Fatalf ("Upload returned error: %v" , err )
222240 }
223- if ! strings .Contains (uploadedPath , "/bucket/prefix/provider=claude_code_web/user_id=user/run_id= run/runtime .jsonl" ) {
241+ if ! strings .Contains (uploadedPath , "/bucket/prefix/runtime/date=2026-07-12/1783887423-claude_code_web- run.jsonl.gz " ) {
224242 t .Fatalf ("upload path = %q" , uploadedPath )
225243 }
226244 if ! strings .HasPrefix (uploadedAuth , "AWS4-HMAC-SHA256 Credential=AKIATEST/" ) {
227245 t .Fatalf ("Authorization = %q" , uploadedAuth )
228246 }
229- for _ , want := range []string {"us-west-2/s3/aws4_request" , "SignedHeaders=content-type;host;x-amz-content-sha256;x-amz-date;x-amz-security-token" } {
247+ for _ , want := range []string {"us-west-2/s3/aws4_request" , "SignedHeaders=content-encoding;content- type;host;x-amz-content-sha256;x-amz-date;x-amz-security-token" } {
230248 if ! strings .Contains (uploadedAuth , want ) {
231249 t .Fatalf ("Authorization missing %q: %q" , want , uploadedAuth )
232250 }
233251 }
234252 if uploadedDate == "" {
235253 t .Fatal ("X-Amz-Date is empty" )
236254 }
237- sum := sha256 .Sum256 ([] byte ( body ) )
255+ sum := sha256 .Sum256 (uploadedBody )
238256 if uploadedHash != hex .EncodeToString (sum [:]) {
239257 t .Fatalf ("X-Amz-Content-Sha256 = %q" , uploadedHash )
240258 }
@@ -244,8 +262,11 @@ func TestUploadSendsJSONLToS3(t *testing.T) {
244262 if uploadedType != contentTypeJSONL {
245263 t .Fatalf ("Content-Type = %q, want %q" , uploadedType , contentTypeJSONL )
246264 }
247- if uploadedBody != body {
248- t .Fatalf ("uploaded body = %q" , uploadedBody )
265+ if uploadedEncoding != contentEncoding {
266+ t .Fatalf ("Content-Encoding = %q, want %q" , uploadedEncoding , contentEncoding )
267+ }
268+ if got := gunzipString (t , uploadedBody ); got != body {
269+ t .Fatalf ("uploaded body = %q" , got )
249270 }
250271}
251272
@@ -280,6 +301,27 @@ func mustRSAKey(t *testing.T) *rsa.PrivateKey {
280301 return key
281302}
282303
304+ func stubNow (t * testing.T , value time.Time ) func () {
305+ t .Helper ()
306+ old := nowUTC
307+ nowUTC = func () time.Time { return value .UTC () }
308+ return func () { nowUTC = old }
309+ }
310+
311+ func gunzipString (t * testing.T , data []byte ) string {
312+ t .Helper ()
313+ reader , err := gzip .NewReader (bytes .NewReader (data ))
314+ if err != nil {
315+ t .Fatalf ("gzip reader: %v" , err )
316+ }
317+ defer reader .Close ()
318+ out , err := io .ReadAll (reader )
319+ if err != nil {
320+ t .Fatalf ("gzip read: %v" , err )
321+ }
322+ return string (out )
323+ }
324+
283325func pemKey (t * testing.T , key * rsa.PrivateKey ) string {
284326 t .Helper ()
285327 data , err := x509 .MarshalPKCS8PrivateKey (key )
0 commit comments