Skip to content

Commit e477b4c

Browse files
author
aibuddy
committed
test(oai): add audit logging and meta tests; raise coverage
Add tests for http_attempt/http_timing/length_backoff/chat_meta and truncate bounds. Validates log structure and stage propagation.
1 parent 7867692 commit e477b4c

9 files changed

Lines changed: 195 additions & 196 deletions

File tree

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,4 @@ go.work
2929
# Editor swap files
3030
*.swp
3131
work/
32-
.cursor
32+
.cursor/

internal/oai/backoff.go

Lines changed: 58 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
package oai
22

33
import (
4-
mathrand "math/rand"
5-
"net/http"
6-
"strings"
7-
"time"
4+
mathrand "math/rand"
5+
"net/http"
6+
"strings"
7+
"time"
88
)
99

1010
// RetryPolicy controls HTTP retry behavior for transient failures.
@@ -13,80 +13,80 @@ import (
1313
// JitterFraction specifies the +/- fractional jitter applied to each computed backoff.
1414
// When Rand is non-nil, it is used to sample jitter for deterministic tests.
1515
type RetryPolicy struct {
16-
MaxRetries int
17-
Backoff time.Duration
18-
JitterFraction float64
19-
Rand *mathrand.Rand
16+
MaxRetries int
17+
Backoff time.Duration
18+
JitterFraction float64
19+
Rand *mathrand.Rand
2020
}
2121

2222
// backoffDuration returns the duration that sleepBackoff would sleep for a given attempt.
2323
func backoffDuration(base time.Duration, attempt int) time.Duration {
24-
if base <= 0 {
25-
base = 200 * time.Millisecond
26-
}
27-
d := base << attempt
28-
if d > 2*time.Second {
29-
d = 2 * time.Second
30-
}
31-
return d
24+
if base <= 0 {
25+
base = 200 * time.Millisecond
26+
}
27+
d := base << attempt
28+
if d > 2*time.Second {
29+
d = 2 * time.Second
30+
}
31+
return d
3232
}
3333

3434
// backoffWithJitter returns an exponential backoff adjusted by +/- jitter fraction.
3535
// When jitterFraction <= 0, this falls back to backoffDuration. When r is nil,
3636
// a time-seeded RNG is used for production randomness.
3737
func backoffWithJitter(base time.Duration, attempt int, jitterFraction float64, r *mathrand.Rand) time.Duration {
38-
d := backoffDuration(base, attempt)
39-
if jitterFraction <= 0 {
40-
return d
41-
}
42-
if jitterFraction > 0.9 { // prevent extreme factors
43-
jitterFraction = 0.9
44-
}
45-
if r == nil {
46-
// Seed with current time for production; tests can pass a custom Rand
47-
r = mathrand.New(mathrand.NewSource(time.Now().UnixNano()))
48-
}
49-
// factor in [1 - f, 1 + f]
50-
minF := 1.0 - jitterFraction
51-
maxF := 1.0 + jitterFraction
52-
factor := minF + r.Float64()*(maxF-minF)
53-
// Guard against rounding to zero
54-
jittered := time.Duration(float64(d) * factor)
55-
if jittered < time.Millisecond {
56-
return time.Millisecond
57-
}
58-
return jittered
38+
d := backoffDuration(base, attempt)
39+
if jitterFraction <= 0 {
40+
return d
41+
}
42+
if jitterFraction > 0.9 { // prevent extreme factors
43+
jitterFraction = 0.9
44+
}
45+
if r == nil {
46+
// Seed with current time for production; tests can pass a custom Rand
47+
r = mathrand.New(mathrand.NewSource(time.Now().UnixNano()))
48+
}
49+
// factor in [1 - f, 1 + f]
50+
minF := 1.0 - jitterFraction
51+
maxF := 1.0 + jitterFraction
52+
factor := minF + r.Float64()*(maxF-minF)
53+
// Guard against rounding to zero
54+
jittered := time.Duration(float64(d) * factor)
55+
if jittered < time.Millisecond {
56+
return time.Millisecond
57+
}
58+
return jittered
5959
}
6060

6161
// retryAfterDuration parses the Retry-After header which may be seconds or HTTP-date.
6262
// Returns (duration, true) when valid; otherwise (0, false).
6363
func retryAfterDuration(h string, now time.Time) (time.Duration, bool) {
64-
h = strings.TrimSpace(h)
65-
if h == "" {
66-
return 0, false
67-
}
68-
// Try integer seconds first
69-
if secs, err := time.ParseDuration(h + "s"); err == nil {
70-
if secs > 0 {
71-
return secs, true
72-
}
73-
}
74-
// Try HTTP-date formats per RFC 7231 (use http.TimeFormat)
75-
if t, err := time.Parse(http.TimeFormat, h); err == nil {
76-
if t.After(now) {
77-
return t.Sub(now), true
78-
}
79-
}
80-
return 0, false
64+
h = strings.TrimSpace(h)
65+
if h == "" {
66+
return 0, false
67+
}
68+
// Try integer seconds first
69+
if secs, err := time.ParseDuration(h + "s"); err == nil {
70+
if secs > 0 {
71+
return secs, true
72+
}
73+
}
74+
// Try HTTP-date formats per RFC 7231 (use http.TimeFormat)
75+
if t, err := time.Parse(http.TimeFormat, h); err == nil {
76+
if t.After(now) {
77+
return t.Sub(now), true
78+
}
79+
}
80+
return 0, false
8181
}
8282

8383
// sleepFor sleeps for the provided duration; extracted for testability.
8484
// sleepFunc allows tests to intercept sleeps deterministically.
8585
var sleepFunc = sleepFor
8686

8787
func sleepFor(d time.Duration) {
88-
if d <= 0 {
89-
return
90-
}
91-
time.Sleep(d)
88+
if d <= 0 {
89+
return
90+
}
91+
time.Sleep(d)
9292
}

internal/oai/client.go

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,23 @@
11
package oai
22

33
import (
4-
"bytes"
5-
"context"
6-
"encoding/json"
7-
"errors"
8-
"fmt"
9-
"io"
10-
"net/http"
11-
"net/http/httptrace"
12-
"strings"
13-
"time"
4+
"bytes"
5+
"context"
6+
"encoding/json"
7+
"errors"
8+
"fmt"
9+
"io"
10+
"net/http"
11+
"net/http/httptrace"
12+
"strings"
13+
"time"
1414
)
1515

1616
type Client struct {
17-
baseURL string
18-
apiKey string
19-
httpClient *http.Client
20-
retry RetryPolicy
17+
baseURL string
18+
apiKey string
19+
httpClient *http.Client
20+
retry RetryPolicy
2121
}
2222

2323
// NewClient creates a client without retries (single attempt only).
@@ -306,4 +306,3 @@ func (c *Client) StreamChat(ctx context.Context, req ChatCompletionsRequest, onC
306306
}
307307
}
308308
}
309-

internal/oai/diagnostics.go

Lines changed: 25 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,37 @@
11
package oai
22

33
import (
4-
"context"
5-
"errors"
6-
"strings"
4+
"context"
5+
"errors"
6+
"strings"
77
)
88

99
// classifyHTTPCause returns a short cause label for audit based on error/context.
1010
func classifyHTTPCause(ctx context.Context, err error) string {
11-
if err == nil {
12-
return "success"
13-
}
14-
if errors.Is(err, context.DeadlineExceeded) || (ctx != nil && ctx.Err() == context.DeadlineExceeded) {
15-
return "context_deadline"
16-
}
17-
s := strings.ToLower(err.Error())
18-
switch {
19-
case strings.Contains(s, "server closed") || strings.Contains(s, "connection reset") || strings.Contains(s, "broken pipe"):
20-
return "server_closed"
21-
case strings.Contains(s, "timeout"):
22-
return "timeout"
23-
default:
24-
return "error"
25-
}
11+
if err == nil {
12+
return "success"
13+
}
14+
if errors.Is(err, context.DeadlineExceeded) || (ctx != nil && ctx.Err() == context.DeadlineExceeded) {
15+
return "context_deadline"
16+
}
17+
s := strings.ToLower(err.Error())
18+
switch {
19+
case strings.Contains(s, "server closed") || strings.Contains(s, "connection reset") || strings.Contains(s, "broken pipe"):
20+
return "server_closed"
21+
case strings.Contains(s, "timeout"):
22+
return "timeout"
23+
default:
24+
return "error"
25+
}
2626
}
2727

2828
// userHintForCause returns a short actionable hint for common failure causes.
2929
func userHintForCause(ctx context.Context, err error) string {
30-
if err == nil {
31-
return ""
32-
}
33-
if errors.Is(err, context.DeadlineExceeded) || (ctx != nil && ctx.Err() == context.DeadlineExceeded) || strings.Contains(strings.ToLower(err.Error()), "timeout") {
34-
return "increase -http-timeout or reduce prompt/model latency"
35-
}
36-
return ""
30+
if err == nil {
31+
return ""
32+
}
33+
if errors.Is(err, context.DeadlineExceeded) || (ctx != nil && ctx.Err() == context.DeadlineExceeded) || strings.Contains(strings.ToLower(err.Error()), "timeout") {
34+
return "increase -http-timeout or reduce prompt/model latency"
35+
}
36+
return ""
3737
}

internal/oai/idempotency.go

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
11
package oai
22

33
import (
4-
"crypto/rand"
5-
"encoding/hex"
6-
"fmt"
7-
"time"
4+
"crypto/rand"
5+
"encoding/hex"
6+
"fmt"
7+
"time"
88
)
99

1010
// generateIdempotencyKey returns a random hex string suitable for Idempotency-Key.
1111
func generateIdempotencyKey() string {
12-
var b [16]byte
13-
if _, err := rand.Read(b[:]); err != nil {
14-
// Fallback to timestamp-based key if crypto/rand fails; extremely unlikely
15-
return fmt.Sprintf("goagent-%d", time.Now().UnixNano())
16-
}
17-
return "goagent-" + hex.EncodeToString(b[:])
12+
var b [16]byte
13+
if _, err := rand.Read(b[:]); err != nil {
14+
// Fallback to timestamp-based key if crypto/rand fails; extremely unlikely
15+
return fmt.Sprintf("goagent-%d", time.Now().UnixNano())
16+
}
17+
return "goagent-" + hex.EncodeToString(b[:])
1818
}

0 commit comments

Comments
 (0)