@pi-in-go/pigpen-jev 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. package/CREDITS.md +22 -0
  2. package/LICENSE +22 -0
  3. package/README.md +237 -0
  4. package/extensions/jev/ask.go +166 -0
  5. package/extensions/jev/ask_test.go +218 -0
  6. package/extensions/jev/backend.go +128 -0
  7. package/extensions/jev/bench_test.go +64 -0
  8. package/extensions/jev/boundaries_test.go +159 -0
  9. package/extensions/jev/command.go +224 -0
  10. package/extensions/jev/commands_test.go +214 -0
  11. package/extensions/jev/config.go +450 -0
  12. package/extensions/jev/errors_test.go +191 -0
  13. package/extensions/jev/extension.go +391 -0
  14. package/extensions/jev/fakehost_test.go +548 -0
  15. package/extensions/jev/gate.go +125 -0
  16. package/extensions/jev/gate_test.go +610 -0
  17. package/extensions/jev/gatekey_test.go +24 -0
  18. package/extensions/jev/go.mod +9 -0
  19. package/extensions/jev/go.sum +2 -0
  20. package/extensions/jev/go.work +10 -0
  21. package/extensions/jev/helpers_test.go +404 -0
  22. package/extensions/jev/memo.go +88 -0
  23. package/extensions/jev/output.go +89 -0
  24. package/extensions/jev/output_test.go +187 -0
  25. package/extensions/jev/ownmodel_test.go +118 -0
  26. package/extensions/jev/render.go +136 -0
  27. package/extensions/jev/review_test.go +310 -0
  28. package/extensions/jev/source_test.go +57 -0
  29. package/extensions/jev/text.go +174 -0
  30. package/extensions/jev/trust_test.go +335 -0
  31. package/extensions/jev/types.go +227 -0
  32. package/libs/typesafe/CONTRACT.md +125 -0
  33. package/libs/typesafe/CREDITS.md +37 -0
  34. package/libs/typesafe/LICENSE +23 -0
  35. package/libs/typesafe/README.md +19 -0
  36. package/libs/typesafe/go.mod +3 -0
  37. package/libs/typesafe/libraries/ownmodel/backend_test.go +496 -0
  38. package/libs/typesafe/libraries/ownmodel/canon.go +190 -0
  39. package/libs/typesafe/libraries/ownmodel/convert.go +199 -0
  40. package/libs/typesafe/libraries/ownmodel/doc.go +15 -0
  41. package/libs/typesafe/libraries/ownmodel/equivalence_test.go +199 -0
  42. package/libs/typesafe/libraries/ownmodel/helpers_test.go +155 -0
  43. package/libs/typesafe/libraries/ownmodel/mutation_test.go +31 -0
  44. package/libs/typesafe/libraries/ownmodel/ownmodel.go +225 -0
  45. package/libs/typesafe/libraries/ownmodel/plan.go +442 -0
  46. package/libs/typesafe/libraries/ownmodel/run.go +288 -0
  47. package/libs/typesafe/libraries/ownmodel/schema_test.go +254 -0
  48. package/libs/typesafe/libraries/ownmodel/twins_test.go +169 -0
  49. package/libs/typesafe/libraries/ownmodel/utils_test.go +125 -0
  50. package/libs/typesafe/libraries/pigmodel/pigmodel.go +264 -0
  51. package/libs/typesafe/libraries/pigmodel/pigmodel_test.go +410 -0
  52. package/libs/typesafe/libraries/typesafe/answers.go +268 -0
  53. package/libs/typesafe/libraries/typesafe/api_response_test.go +113 -0
  54. package/libs/typesafe/libraries/typesafe/batch.go +80 -0
  55. package/libs/typesafe/libraries/typesafe/batch_test.go +133 -0
  56. package/libs/typesafe/libraries/typesafe/bench_test.go +71 -0
  57. package/libs/typesafe/libraries/typesafe/client.go +561 -0
  58. package/libs/typesafe/libraries/typesafe/client_test.go +495 -0
  59. package/libs/typesafe/libraries/typesafe/crosscheck_test.go +464 -0
  60. package/libs/typesafe/libraries/typesafe/crosscheck_workflowevals_test.go +219 -0
  61. package/libs/typesafe/libraries/typesafe/doc.go +27 -0
  62. package/libs/typesafe/libraries/typesafe/entry.go +142 -0
  63. package/libs/typesafe/libraries/typesafe/env.go +11 -0
  64. package/libs/typesafe/libraries/typesafe/errors.go +310 -0
  65. package/libs/typesafe/libraries/typesafe/errors_test.go +175 -0
  66. package/libs/typesafe/libraries/typesafe/helpers_test.go +294 -0
  67. package/libs/typesafe/libraries/typesafe/live_test.go +96 -0
  68. package/libs/typesafe/libraries/typesafe/logging.go +160 -0
  69. package/libs/typesafe/libraries/typesafe/logging_test.go +259 -0
  70. package/libs/typesafe/libraries/typesafe/marshal_test.go +112 -0
  71. package/libs/typesafe/libraries/typesafe/mutation_test.go +39 -0
  72. package/libs/typesafe/libraries/typesafe/questions.go +490 -0
  73. package/libs/typesafe/libraries/typesafe/questions_test.go +166 -0
  74. package/libs/typesafe/libraries/typesafe/regressions_test.go +159 -0
  75. package/libs/typesafe/libraries/typesafe/reliability_test.go +649 -0
  76. package/libs/typesafe/libraries/typesafe/retry.go +350 -0
  77. package/libs/typesafe/libraries/typesafe/retry_test.go +297 -0
  78. package/libs/typesafe/libraries/typesafe/runtime_test.go +26 -0
  79. package/libs/typesafe/libraries/typesafe/transport_test.go +163 -0
  80. package/libs/typesafe/libraries/typesafe/twins_test.go +127 -0
  81. package/libs/typesafe/libraries/typesafe/types_test.go +165 -0
  82. package/libs/typesafe/libraries/typesafe/version.go +10 -0
  83. package/libs/typesafe/package.json +37 -0
  84. package/libs/typesafe/provenance.json +49 -0
  85. package/package.json +42 -0
  86. package/port/PORT.md +107 -0
  87. package/port/e2e/gate-and-output.py +35 -0
  88. package/port/e2e/jev-ask.py +36 -0
  89. package/port/e2e/model-switch.py +44 -0
  90. package/port/e2e/off-by-default.py +34 -0
  91. package/port/gen-scenarios.py +103 -0
  92. package/port/golden/cache-identical-calls.jsonl +30 -0
  93. package/port/golden/clear.jsonl +22 -0
  94. package/port/golden/commands.jsonl +43 -0
  95. package/port/golden/enforce-accept.jsonl +23 -0
  96. package/port/golden/enforce-decline.jsonl +22 -0
  97. package/port/golden/jev-ask.jsonl +20 -0
  98. package/port/golden/output-advice.jsonl +23 -0
  99. package/port/golden/output-leak.jsonl +24 -0
  100. package/port/golden/output-low-confidence.jsonl +22 -0
  101. package/port/golden/shadow-flagged.jsonl +23 -0
  102. package/port/golden/unjudged-tools.jsonl +19 -0
  103. package/port/golden/write-elision.jsonl +21 -0
  104. package/port/mutate-unit.py +63 -0
  105. package/port/mutations.json +578 -0
  106. package/port/oracle/LICENSE +21 -0
  107. package/port/oracle/README.md +181 -0
  108. package/port/oracle/SHA256SUMS +8 -0
  109. package/port/oracle/package.json +43 -0
  110. package/port/oracle/src/client.ts +409 -0
  111. package/port/oracle/src/config.ts +363 -0
  112. package/port/oracle/src/gate.ts +229 -0
  113. package/port/oracle/src/index.ts +649 -0
  114. package/port/oracle/src/output.ts +163 -0
  115. package/port/red-run.log +309 -0
  116. package/port/scenarios/cache-identical-calls.json +71 -0
  117. package/port/scenarios/clear.json +61 -0
  118. package/port/scenarios/commands.json +119 -0
  119. package/port/scenarios/enforce-accept.json +66 -0
  120. package/port/scenarios/enforce-decline.json +57 -0
  121. package/port/scenarios/jev-ask.json +83 -0
  122. package/port/scenarios/output-advice.json +61 -0
  123. package/port/scenarios/output-leak.json +61 -0
  124. package/port/scenarios/output-low-confidence.json +61 -0
  125. package/port/scenarios/shadow-flagged.json +61 -0
  126. package/port/scenarios/unjudged-tools.json +55 -0
  127. package/port/scenarios/write-elision.json +53 -0
  128. package/provenance.json +18 -0
@@ -0,0 +1,649 @@
1
+ package typesafe
2
+
3
+ import (
4
+ "context"
5
+ "errors"
6
+ "net/http"
7
+ "regexp"
8
+ "runtime"
9
+ "testing"
10
+ "testing/synctest"
11
+ "time"
12
+ )
13
+
14
+ // These tests run in a synctest bubble: time is fake, so backoff and timeout delays
15
+ // are exact, the analog of the upstream vi.useFakeTimers().
16
+
17
+ // infoLogger records info, warn and error lines only (the upstream infoLogger).
18
+ func infoLogger() *recordingLogger { return &recordingLogger{} }
19
+
20
+ type result[T any] struct {
21
+ v T
22
+ err error
23
+ }
24
+
25
+ // start runs fn in a goroutine and returns a channel for its result.
26
+ func start[T any](fn func() (T, error)) <-chan result[T] {
27
+ ch := make(chan result[T], 1)
28
+ go func() {
29
+ v, err := fn()
30
+ ch <- result[T]{v, err}
31
+ }()
32
+ return ch
33
+ }
34
+
35
+ func advance(d time.Duration) {
36
+ time.Sleep(d)
37
+ synctest.Wait()
38
+ }
39
+
40
+ func settle[T any](ch <-chan result[T]) (T, error) {
41
+ select {
42
+ case r := <-ch:
43
+ return r.v, r.err
44
+ case <-time.After(time.Hour):
45
+ panic("call never settled")
46
+ }
47
+ }
48
+
49
+ func failing(status int, headers ...string) func() *http.Response {
50
+ return func() *http.Response { return jsonResp(status, map[string]any{}, headers...) }
51
+ }
52
+
53
+ func TestRetries_Retries429ThenSucceedsHonoringRetryAfter(t *testing.T) {
54
+ twin(t, "reliability.test.ts | retries retries a 429 and then succeeds, honoring Retry-After")
55
+ synctest.Test(t, func(t *testing.T) {
56
+ logger := infoLogger()
57
+ calls := 0
58
+ m := newMock(func(*recordedRequest) (*http.Response, error) {
59
+ calls++
60
+ if calls == 1 {
61
+ return jsonResp(429, map[string]any{}, "retry-after", "2"), nil
62
+ }
63
+ return textResp(200, modelsBody, "content-type", "application/json"), nil
64
+ })
65
+ c := newClient(t, m, func(cfg *Config) { cfg.Logger, cfg.LogLevel = logger, LogInfo })
66
+ p := start(func() ([]ModelCard, error) { return c.Models().List(ctxBG(), nil) })
67
+ advance(1999 * time.Millisecond)
68
+ eq(t, m.count(), 1)
69
+ advance(1 * time.Millisecond)
70
+ got, err := settle(p)
71
+ noErr(t, err)
72
+ eq(t, got, modelCards)
73
+ eq(t, m.count(), 2)
74
+ lines := logger.messages("")
75
+ eq(t, len(lines), 3)
76
+ if !regexp.MustCompile(`^#1 GET /v1/models <- 429 in \d+ms$`).MatchString(lines[0]) || !regexp.MustCompile(`^#1 GET /v1/models <- 200 in \d+ms$`).MatchString(lines[2]) {
77
+ t.Fatalf("lines %q", lines)
78
+ }
79
+ eq(t, lines[1], "#1 GET /v1/models retrying in 2000ms (retry 1/2) after 429")
80
+ })
81
+ }
82
+
83
+ func TestRetries_UsesExponentialBackoffWhenThereIsNoRetryAfter(t *testing.T) {
84
+ twin(t, "reliability.test.ts | retries uses exponential backoff when there is no Retry-After")
85
+ synctest.Test(t, func(t *testing.T) {
86
+ calls := 0
87
+ m := newMock(func(*recordedRequest) (*http.Response, error) {
88
+ calls++
89
+ if calls <= 2 {
90
+ return jsonResp(503, map[string]any{}), nil
91
+ }
92
+ return textResp(200, modelsBody, "content-type", "application/json"), nil
93
+ })
94
+ c := newClient(t, m)
95
+ p := start(func() ([]ModelCard, error) { return c.Models().List(ctxBG(), nil) })
96
+ advance(499 * time.Millisecond)
97
+ eq(t, m.count(), 1)
98
+ advance(1 * time.Millisecond)
99
+ eq(t, m.count(), 2)
100
+ advance(999 * time.Millisecond)
101
+ eq(t, m.count(), 2)
102
+ advance(1 * time.Millisecond)
103
+ eq(t, m.count(), 3)
104
+ got, err := settle(p)
105
+ noErr(t, err)
106
+ eq(t, got, modelCards)
107
+ })
108
+ }
109
+
110
+ func TestRetries_GivesUpAfterMaxRetriesAndThrowsTheLastError(t *testing.T) {
111
+ twin(t, "reliability.test.ts | retries gives up after maxRetries and throws the last error")
112
+ synctest.Test(t, func(t *testing.T) {
113
+ m := always(func() *http.Response { return jsonResp(500, map[string]any{"message": "down"}) })
114
+ c := newClient(t, m, func(cfg *Config) { cfg.Retry = RetryOverrides{MaxRetries: Ptr(3)} })
115
+ _, err := c.Models().List(ctxBG(), nil)
116
+ mustAs[*InternalServerError](t, err)
117
+ eq(t, m.count(), 4)
118
+ })
119
+ }
120
+
121
+ func TestRetries_DoesNotRetryNonRetryableStatuses(t *testing.T) {
122
+ twin(t, "reliability.test.ts | retries does not retry non-retryable statuses")
123
+ synctest.Test(t, func(t *testing.T) {
124
+ m := always(failing(400))
125
+ _, err := newClient(t, m).Models().List(ctxBG(), nil)
126
+ mustAs[*BadRequestError](t, err)
127
+ eq(t, m.count(), 1)
128
+ })
129
+ }
130
+
131
+ func TestRetries_RetriesConnectionErrors(t *testing.T) {
132
+ twin(t, "reliability.test.ts | retries retries connection errors")
133
+ synctest.Test(t, func(t *testing.T) {
134
+ calls := 0
135
+ m := newMock(func(*recordedRequest) (*http.Response, error) {
136
+ calls++
137
+ if calls == 1 {
138
+ return nil, errors.New("fetch failed")
139
+ }
140
+ return textResp(200, modelsBody, "content-type", "application/json"), nil
141
+ })
142
+ got, err := newClient(t, m).Models().List(ctxBG(), nil)
143
+ noErr(t, err)
144
+ eq(t, got, modelCards)
145
+ eq(t, m.count(), 2)
146
+ })
147
+ }
148
+
149
+ func TestRetries_NeverRetriesACallerAbort(t *testing.T) {
150
+ twin(t, "reliability.test.ts | retries never retries a caller abort")
151
+ synctest.Test(t, func(t *testing.T) {
152
+ ctx, cancel := context.WithCancel(context.Background())
153
+ m := newMock(func(*recordedRequest) (*http.Response, error) { cancel(); return nil, context.Canceled })
154
+ _, err := newClient(t, m).Models().List(ctx, nil)
155
+ mustAs[*APIUserAbortError](t, err)
156
+ eq(t, m.count(), 1)
157
+ })
158
+ }
159
+
160
+ func TestRetries_AbortingDuringTheBackoffWaitThrowsAPIUserAbortError(t *testing.T) {
161
+ twin(t, "reliability.test.ts | retries aborting during the backoff wait throws APIUserAbortError")
162
+ synctest.Test(t, func(t *testing.T) {
163
+ ctx, cancel := context.WithCancel(context.Background())
164
+ m := always(failing(503))
165
+ c := newClient(t, m)
166
+ p := start(func() ([]ModelCard, error) { return c.Models().List(ctx, nil) })
167
+ advance(100 * time.Millisecond)
168
+ cancel()
169
+ _, err := settle(p)
170
+ mustAs[*APIUserAbortError](t, err)
171
+ eq(t, m.count(), 1)
172
+ })
173
+ }
174
+
175
+ func TestRetries_PerCallMaxRetriesOverridesTheClientAndZeroDisablesRetries(t *testing.T) {
176
+ twin(t, "reliability.test.ts | retries per-call maxRetries overrides the client, and 0 disables retries")
177
+ synctest.Test(t, func(t *testing.T) {
178
+ m := always(failing(503))
179
+ c := newClient(t, m, func(cfg *Config) { cfg.Retry = RetryOverrides{MaxRetries: Ptr(5)} })
180
+ _, err := c.Models().List(ctxBG(), &RequestOptions{Retry: RetryOverrides{MaxRetries: Ptr(0)}})
181
+ mustAs[*InternalServerError](t, err)
182
+ eq(t, m.count(), 1)
183
+ })
184
+ }
185
+
186
+ func TestRetries_TellsTheServerWhichRetryThisIs(t *testing.T) {
187
+ twin(t, "reliability.test.ts | retries tells the server which retry this is")
188
+ synctest.Test(t, func(t *testing.T) {
189
+ calls := 0
190
+ m := newMock(func(*recordedRequest) (*http.Response, error) {
191
+ calls++
192
+ if calls <= 2 {
193
+ return jsonResp(503, map[string]any{}), nil
194
+ }
195
+ return textResp(200, modelsBody, "content-type", "application/json"), nil
196
+ })
197
+ _, err := newClient(t, m).Models().List(ctxBG(), nil)
198
+ noErr(t, err)
199
+ eq(t, m.req(0).Header.Get("X-TypeSafe-Retry-Count"), "")
200
+ eq(t, m.req(1).Header.Get("X-TypeSafe-Retry-Count"), "1")
201
+ eq(t, m.req(2).Header.Get("X-TypeSafe-Retry-Count"), "2")
202
+ })
203
+ }
204
+
205
+ func TestRetries_RejectsInvalidMaxRetriesFromConfigOrPerCall(t *testing.T) {
206
+ // Adapted: 1.5 is not representable (MaxRetries is an int); the messages name the Go fields.
207
+ twin(t, "reliability.test.ts | retries rejects invalid maxRetries from config or per call")
208
+ _, err := NewClient(Config{APIKey: "k", Getenv: noEnv, Retry: RetryOverrides{MaxRetries: Ptr(-1)}})
209
+ mustAs[*TypeSafeError](t, err)
210
+ contains(t, err.Error(), "Retry.MaxRetries")
211
+ c := newClient(t, modelsOK())
212
+ _, err = c.Models().List(ctxBG(), &RequestOptions{Retry: RetryOverrides{MaxRetries: Ptr(-1)}})
213
+ mustAs[*TypeSafeError](t, err)
214
+ contains(t, err.Error(), "Retry.MaxRetries")
215
+ }
216
+
217
+ func TestRetryPolicy_DefaultsToTheSDKPolicyAndExposesTheResolvedPolicyOnTheClient(t *testing.T) {
218
+ twin(t, "reliability.test.ts | retry policy defaults to the SDK policy and exposes the resolved policy on the client")
219
+ c, err := NewClient(Config{APIKey: "k", Getenv: noEnv})
220
+ noErr(t, err)
221
+ eq(t, c.RetryPolicy(), DefaultRetryPolicy())
222
+ c, err = NewClient(Config{APIKey: "k", Getenv: noEnv, Retry: RetryOverrides{MaxRetries: Ptr(7), BackoffJitter: Ptr(0.0)}})
223
+ noErr(t, err)
224
+ want := DefaultRetryPolicy()
225
+ want.MaxRetries, want.BackoffJitter = 7, 0
226
+ eq(t, c.RetryPolicy(), want)
227
+ }
228
+
229
+ func TestRetryPolicy_IsolatesThePolicyAndStatusSetBetweenDefaultClients(t *testing.T) {
230
+ twin(t, "reliability.test.ts | retry policy isolates the policy and status set between default clients")
231
+ first, _ := NewClient(Config{APIKey: "k", Getenv: noEnv})
232
+ second, _ := NewClient(Config{APIKey: "k", Getenv: noEnv})
233
+ // Callers can mutate what the accessor returns; that must not reach the client.
234
+ p := first.RetryPolicy()
235
+ p.MaxRetries = 0
236
+ p.HTTPStatuses[0] = 0
237
+ p.HTTPStatuses = p.HTTPStatuses[:0]
238
+ d := DefaultRetryPolicy()
239
+ d.HTTPStatuses[0] = 1
240
+ for _, c := range []*Client{first, second, mustClient(t, Config{APIKey: "k", Getenv: noEnv})} {
241
+ got := c.RetryPolicy()
242
+ eq(t, got.MaxRetries, 2)
243
+ if !got.RetriesStatus(503) || !got.RetriesStatus(408) {
244
+ t.Fatal("the status set changed")
245
+ }
246
+ }
247
+ }
248
+
249
+ func mustClient(t testing.TB, cfg Config) *Client {
250
+ t.Helper()
251
+ c, err := NewClient(cfg)
252
+ noErr(t, err)
253
+ return c
254
+ }
255
+
256
+ func TestRetryPolicy_CopiesCallerOwnedStatusSetsWhenConstructingAClient(t *testing.T) {
257
+ twin(t, "reliability.test.ts | retry policy copies caller-owned status sets when constructing a client")
258
+ synctest.Test(t, func(t *testing.T) {
259
+ statuses := []int{503}
260
+ m := always(failing(503))
261
+ c := newClient(t, m, func(cfg *Config) { cfg.Retry = RetryOverrides{HTTPStatuses: statuses, MaxRetries: Ptr(1)} })
262
+ statuses[0] = 0
263
+ _, err := c.Models().List(ctxBG(), nil)
264
+ mustAs[*InternalServerError](t, err)
265
+ eq(t, m.count(), 2)
266
+ })
267
+ }
268
+
269
+ func TestRetryPolicy_SnapshotsThePolicyForAnInFlightRequest(t *testing.T) {
270
+ twin(t,
271
+ "reliability.test.ts | retry policy snapshots the client policy for an in-flight request",
272
+ "reliability.test.ts | retry policy snapshots the call policy for an in-flight request")
273
+ for _, source := range []string{"client", "call"} {
274
+ synctest.Test(t, func(t *testing.T) {
275
+ statuses := []int{503}
276
+ m := always(failing(503))
277
+ c := newClient(t, m, func(cfg *Config) { cfg.Retry = RetryOverrides{MaxRetries: Ptr(1), HTTPStatuses: []int{503}} })
278
+ var opts *RequestOptions
279
+ if source == "call" {
280
+ opts = &RequestOptions{Retry: RetryOverrides{HTTPStatuses: statuses}}
281
+ }
282
+ p := start(func() ([]ModelCard, error) { return c.Models().List(ctxBG(), opts) })
283
+ synctest.Wait() // first attempt is in its backoff wait
284
+ // Mutating what the client exposes, or the caller's slice, must not change the call.
285
+ pol := c.RetryPolicy()
286
+ pol.MaxRetries = 0
287
+ pol.HTTPStatuses = nil
288
+ statuses[0] = 0
289
+ _, err := settle(p)
290
+ mustAs[*InternalServerError](t, err)
291
+ eq(t, m.count(), 2)
292
+ })
293
+ }
294
+ }
295
+
296
+ func TestRetryPolicy_CanExtendTheDefaultStatusesBySpreadingTheClientsPolicy(t *testing.T) {
297
+ twin(t, "reliability.test.ts | retry policy can extend the default statuses by spreading the client's policy")
298
+ client := mustClient(t, Config{APIKey: "k", Getenv: noEnv})
299
+ extended := mustClient(t, Config{APIKey: "k", Getenv: noEnv, Retry: RetryOverrides{HTTPStatuses: append(client.RetryPolicy().HTTPStatuses, 409)}})
300
+ if !extended.RetryPolicy().RetriesStatus(409) || !extended.RetryPolicy().RetriesStatus(503) {
301
+ t.Fatal("extended set must retry 409 and 503")
302
+ }
303
+ if client.RetryPolicy().RetriesStatus(409) {
304
+ t.Fatal("the original client must not retry 409")
305
+ }
306
+ }
307
+
308
+ func TestRetryPolicy_RetriesOnlyTheStatusesInHTTPStatuses(t *testing.T) {
309
+ twin(t, "reliability.test.ts | retry policy retries only the statuses in httpStatuses")
310
+ synctest.Test(t, func(t *testing.T) {
311
+ m := always(failing(409))
312
+ c := newClient(t, m, func(cfg *Config) { cfg.Retry = RetryOverrides{HTTPStatuses: []int{409}} })
313
+ _, err := c.Models().List(ctxBG(), nil)
314
+ mustAs[*APIError](t, err)
315
+ eq(t, m.count(), 3)
316
+
317
+ server := always(failing(503))
318
+ none := newClient(t, server, func(cfg *Config) { cfg.Retry = RetryOverrides{HTTPStatuses: []int{}} })
319
+ _, err = none.Models().List(ctxBG(), nil)
320
+ mustAs[*InternalServerError](t, err)
321
+ eq(t, server.count(), 1)
322
+ })
323
+ }
324
+
325
+ func TestRetryPolicy_CanStopRetryingConnectionErrorsWhileStillRetryingTimeouts(t *testing.T) {
326
+ twin(t, "reliability.test.ts | retry policy can stop retrying connection errors while still retrying timeouts")
327
+ synctest.Test(t, func(t *testing.T) {
328
+ dropped := newMock(func(*recordedRequest) (*http.Response, error) { return nil, errors.New("fetch failed") })
329
+ c := newClient(t, dropped, func(cfg *Config) { cfg.Retry = RetryOverrides{APIConnectionError: Ptr(false)} })
330
+ _, err := c.Models().List(ctxBG(), nil)
331
+ mustAs[*APIConnectionError](t, err)
332
+ eq(t, dropped.count(), 1)
333
+
334
+ hung := hangingDoer()
335
+ timeouts := newClient(t, hung, func(cfg *Config) {
336
+ cfg.Timeout = 10 * time.Millisecond
337
+ cfg.Retry = RetryOverrides{APIConnectionError: Ptr(false), MaxRetries: Ptr(1)}
338
+ })
339
+ _, err = timeouts.Models().List(ctxBG(), nil)
340
+ mustAs[*APITimeoutError](t, err)
341
+ eq(t, hung.count(), 2)
342
+ })
343
+ }
344
+
345
+ func TestRetryPolicy_CanStopRetryingTimeoutsWhileStillRetryingConnectionErrors(t *testing.T) {
346
+ twin(t, "reliability.test.ts | retry policy can stop retrying timeouts while still retrying connection errors")
347
+ synctest.Test(t, func(t *testing.T) {
348
+ hung := hangingDoer()
349
+ c := newClient(t, hung, func(cfg *Config) {
350
+ cfg.Timeout = 10 * time.Millisecond
351
+ cfg.Retry = RetryOverrides{APITimeoutError: Ptr(false)}
352
+ })
353
+ _, err := c.Models().List(ctxBG(), nil)
354
+ mustAs[*APITimeoutError](t, err)
355
+ eq(t, hung.count(), 1)
356
+
357
+ dropped := newMock(func(*recordedRequest) (*http.Response, error) { return nil, errors.New("fetch failed") })
358
+ conns := newClient(t, dropped, func(cfg *Config) { cfg.Retry = RetryOverrides{APITimeoutError: Ptr(false), MaxRetries: Ptr(1)} })
359
+ _, err = conns.Models().List(ctxBG(), nil)
360
+ mustAs[*APIConnectionError](t, err)
361
+ eq(t, dropped.count(), 2)
362
+ })
363
+ }
364
+
365
+ func TestRetryPolicy_BacksOffFromTheConfiguredInitialDelayAndCap(t *testing.T) {
366
+ twin(t, "reliability.test.ts | retry policy backs off from the configured initial delay and cap")
367
+ synctest.Test(t, func(t *testing.T) {
368
+ calls := 0
369
+ m := newMock(func(*recordedRequest) (*http.Response, error) {
370
+ calls++
371
+ if calls <= 3 {
372
+ return jsonResp(503, map[string]any{}), nil
373
+ }
374
+ return textResp(200, modelsBody, "content-type", "application/json"), nil
375
+ })
376
+ c := newClient(t, m, func(cfg *Config) {
377
+ cfg.Retry = RetryOverrides{MaxRetries: Ptr(3), BackoffInitial: Ptr(100 * time.Millisecond), BackoffMax: Ptr(150 * time.Millisecond)}
378
+ })
379
+ p := start(func() ([]ModelCard, error) { return c.Models().List(ctxBG(), nil) })
380
+ advance(99 * time.Millisecond)
381
+ eq(t, m.count(), 1)
382
+ advance(1 * time.Millisecond)
383
+ eq(t, m.count(), 2)
384
+ advance(150 * time.Millisecond)
385
+ eq(t, m.count(), 3)
386
+ advance(150 * time.Millisecond)
387
+ eq(t, m.count(), 4)
388
+ got, err := settle(p)
389
+ noErr(t, err)
390
+ eq(t, got, modelCards)
391
+ })
392
+ }
393
+
394
+ func TestRetryPolicy_CanIgnoreRetryAfterAndCapsHowLongARetryAfterMayBe(t *testing.T) {
395
+ twin(t, "reliability.test.ts | retry policy can ignore Retry-After, and caps how long a Retry-After may be")
396
+ synctest.Test(t, func(t *testing.T) {
397
+ calls := 0
398
+ m := newMock(func(*recordedRequest) (*http.Response, error) {
399
+ calls++
400
+ if calls%2 == 1 {
401
+ return jsonResp(429, map[string]any{}, "retry-after", "2"), nil
402
+ }
403
+ return textResp(200, modelsBody, "content-type", "application/json"), nil
404
+ })
405
+ c := newClient(t, m, func(cfg *Config) { cfg.Retry = RetryOverrides{RespectRetryAfter: Ptr(false)} })
406
+ p := start(func() ([]ModelCard, error) { return c.Models().List(ctxBG(), nil) })
407
+ advance(500 * time.Millisecond)
408
+ eq(t, m.count(), 2)
409
+ got, err := settle(p)
410
+ noErr(t, err)
411
+ eq(t, got, modelCards)
412
+
413
+ calls = 0
414
+ capped := newClient(t, m, func(cfg *Config) { cfg.Retry = RetryOverrides{MaxRetryAfter: Ptr(1000 * time.Millisecond)} })
415
+ q := start(func() ([]ModelCard, error) { return capped.Models().List(ctxBG(), nil) })
416
+ advance(500 * time.Millisecond)
417
+ eq(t, m.count(), 4)
418
+ got, err = settle(q)
419
+ noErr(t, err)
420
+ eq(t, got, modelCards)
421
+ })
422
+ }
423
+
424
+ func TestRetryPolicy_PerCallRetryOverridesFieldByFieldAndLeavesTheClientsPolicyAlone(t *testing.T) {
425
+ twin(t, "reliability.test.ts | retry policy per-call retry overrides field by field and leaves the client's policy alone")
426
+ synctest.Test(t, func(t *testing.T) {
427
+ logger := infoLogger()
428
+ m := always(failing(503))
429
+ c := newClient(t, m, func(cfg *Config) {
430
+ cfg.Logger, cfg.LogLevel = logger, LogInfo
431
+ cfg.Retry = RetryOverrides{MaxRetries: Ptr(5), BackoffInitial: Ptr(100 * time.Millisecond)}
432
+ })
433
+ _, err := c.Models().List(ctxBG(), &RequestOptions{Retry: RetryOverrides{MaxRetries: Ptr(1)}})
434
+ mustAs[*InternalServerError](t, err)
435
+ eq(t, m.count(), 2)
436
+ found := false
437
+ for _, l := range logger.messages("") {
438
+ if l == "#1 GET /v1/models retrying in 100ms (retry 1/1) after 503" {
439
+ found = true
440
+ }
441
+ }
442
+ if !found {
443
+ t.Fatalf("log lines %q", logger.messages(""))
444
+ }
445
+ eq(t, c.RetryPolicy().MaxRetries, 5)
446
+ })
447
+ }
448
+
449
+ func TestRetryPolicy_ValidatesEveryNumericFieldAndNamesIt(t *testing.T) {
450
+ // Adapted: durations are integers (no NaN or Infinity); the jitter float keeps its NaN and
451
+ // Infinity cases; messages name the Go fields.
452
+ twin(t, "reliability.test.ts | retry policy validates every numeric field and names it")
453
+ bad := func(o RetryOverrides) error {
454
+ _, err := NewClient(Config{APIKey: "k", Getenv: noEnv, Retry: o})
455
+ return err
456
+ }
457
+ mustFail := func(o RetryOverrides, field string) {
458
+ t.Helper()
459
+ err := bad(o)
460
+ if err == nil {
461
+ t.Fatalf("want an error naming %s", field)
462
+ }
463
+ mustAs[*TypeSafeError](t, err)
464
+ contains(t, err.Error(), field)
465
+ }
466
+ mustFail(RetryOverrides{BackoffInitial: Ptr(-time.Millisecond)}, "Retry.BackoffInitial")
467
+ mustFail(RetryOverrides{BackoffMax: Ptr(-time.Millisecond)}, "Retry.BackoffMax")
468
+ mustFail(RetryOverrides{BackoffJitter: Ptr(1.5)}, "Retry.BackoffJitter")
469
+ mustFail(RetryOverrides{BackoffJitter: Ptr(-0.1)}, "Retry.BackoffJitter")
470
+ mustFail(RetryOverrides{BackoffJitter: Ptr(nan())}, "Retry.BackoffJitter")
471
+ mustFail(RetryOverrides{BackoffJitter: Ptr(inf())}, "Retry.BackoffJitter")
472
+ mustFail(RetryOverrides{MaxRetryAfter: Ptr(-time.Millisecond)}, "Retry.MaxRetryAfter")
473
+ mustFail(RetryOverrides{HTTPStatuses: []int{503, 42}}, "Retry.HTTPStatuses")
474
+ mustFail(RetryOverrides{HTTPStatuses: []int{1000}}, "Retry.HTTPStatuses")
475
+ noErr(t, bad(RetryOverrides{BackoffInitial: Ptr(time.Duration(0)), BackoffMax: Ptr(time.Duration(0)), BackoffJitter: Ptr(0.0)}))
476
+ noErr(t, bad(RetryOverrides{BackoffJitter: Ptr(1.0), MaxRetryAfter: Ptr(time.Duration(0))}))
477
+ c := newClient(t, modelsOK())
478
+ _, err := c.Models().List(ctxBG(), &RequestOptions{Retry: RetryOverrides{BackoffJitter: Ptr(2.0)}})
479
+ mustAs[*TypeSafeError](t, err)
480
+ contains(t, err.Error(), "Retry.BackoffJitter")
481
+ }
482
+
483
+ func TestTimeouts_ThrowsAPITimeoutErrorWhichIsAnAPIConnectionError(t *testing.T) {
484
+ twin(t, "reliability.test.ts | timeouts throws APITimeoutError, which is an APIConnectionError")
485
+ synctest.Test(t, func(t *testing.T) {
486
+ c := newClient(t, hangingDoer(), func(cfg *Config) {
487
+ cfg.Timeout = time.Second
488
+ cfg.Retry = RetryOverrides{MaxRetries: Ptr(0)}
489
+ })
490
+ _, err := c.Models().List(ctxBG(), nil)
491
+ to := mustAs[*APITimeoutError](t, err)
492
+ mustAs[*APIConnectionError](t, err)
493
+ eq(t, to.TimeoutMs, int64(1000))
494
+ eq(t, err.Error(), "Request timed out after 1000ms.")
495
+ })
496
+ }
497
+
498
+ func TestTimeouts_FiresAtExactlyTheConfiguredTimeout(t *testing.T) {
499
+ twin(t, "reliability.test.ts | timeouts fires at exactly the configured timeout")
500
+ synctest.Test(t, func(t *testing.T) {
501
+ m := hangingDoer()
502
+ c := newClient(t, m, func(cfg *Config) {
503
+ cfg.Timeout = time.Second
504
+ cfg.Retry = RetryOverrides{MaxRetries: Ptr(0)}
505
+ })
506
+ p := start(func() ([]ModelCard, error) { return c.Models().List(ctxBG(), nil) })
507
+ advance(999 * time.Millisecond)
508
+ if m.req(0).Ctx.Err() != nil {
509
+ t.Fatal("fired early")
510
+ }
511
+ advance(1 * time.Millisecond)
512
+ if m.req(0).Ctx.Err() == nil {
513
+ t.Fatal("did not fire at the timeout")
514
+ }
515
+ _, err := settle(p)
516
+ mustAs[*APITimeoutError](t, err)
517
+ })
518
+ }
519
+
520
+ func TestTimeouts_RetriesAfterATimeoutEachAttemptGettingItsOwnTimeout(t *testing.T) {
521
+ twin(t, "reliability.test.ts | timeouts retries after a timeout, each attempt getting its own timeout")
522
+ synctest.Test(t, func(t *testing.T) {
523
+ calls := 0
524
+ m := newMock(func(r *recordedRequest) (*http.Response, error) {
525
+ calls++
526
+ if calls == 1 {
527
+ <-r.Ctx.Done()
528
+ return nil, r.Ctx.Err()
529
+ }
530
+ return textResp(200, modelsBody, "content-type", "application/json"), nil
531
+ })
532
+ c := newClient(t, m, func(cfg *Config) { cfg.Timeout = time.Second })
533
+ got, err := c.Models().List(ctxBG(), nil)
534
+ noErr(t, err)
535
+ eq(t, got, modelCards)
536
+ eq(t, m.count(), 2)
537
+ })
538
+ }
539
+
540
+ func TestTimeouts_PerCallTimeoutOverridesTheClient(t *testing.T) {
541
+ twin(t, "reliability.test.ts | timeouts per-call timeout overrides the client")
542
+ synctest.Test(t, func(t *testing.T) {
543
+ m := hangingDoer()
544
+ c := newClient(t, m, func(cfg *Config) {
545
+ cfg.Timeout = time.Minute
546
+ cfg.Retry = RetryOverrides{MaxRetries: Ptr(0)}
547
+ })
548
+ p := start(func() ([]ModelCard, error) {
549
+ return c.Models().List(ctxBG(), &RequestOptions{Timeout: 50 * time.Millisecond})
550
+ })
551
+ advance(50 * time.Millisecond)
552
+ if m.req(0).Ctx.Err() == nil {
553
+ t.Fatal("the per-call timeout did not fire")
554
+ }
555
+ _, err := settle(p)
556
+ mustAs[*APITimeoutError](t, err)
557
+ })
558
+ }
559
+
560
+ func TestTimeouts_ACallerAbortDuringAHungRequestIsReportedAsAnAbortNotATimeout(t *testing.T) {
561
+ twin(t, "reliability.test.ts | timeouts a caller abort during a hung request is reported as an abort, not a timeout")
562
+ synctest.Test(t, func(t *testing.T) {
563
+ ctx, cancel := context.WithCancel(context.Background())
564
+ c := newClient(t, hangingDoer(), func(cfg *Config) { cfg.Timeout = time.Minute })
565
+ p := start(func() ([]ModelCard, error) { return c.Models().List(ctx, nil) })
566
+ advance(10 * time.Millisecond)
567
+ cancel()
568
+ _, err := settle(p)
569
+ mustAs[*APIUserAbortError](t, err)
570
+ notAs[*APITimeoutError](t, err)
571
+ })
572
+ }
573
+
574
+ func TestTimeouts_ClearsItsTimerAfterAFastResponse(t *testing.T) {
575
+ // Adapted: no goroutine or timer may outlive the call (the analog of vi.getTimerCount() === 0).
576
+ twin(t, "reliability.test.ts | timeouts clears its timer after a fast response")
577
+ synctest.Test(t, func(t *testing.T) {
578
+ m := always(func() *http.Response { return textResp(200, modelsBody, "content-type", "application/json") })
579
+ c := newClient(t, m, func(cfg *Config) { cfg.Timeout = time.Second })
580
+ before := runtime.NumGoroutine()
581
+ _, err := c.Models().List(ctxBG(), nil)
582
+ noErr(t, err)
583
+ synctest.Wait()
584
+ eq(t, runtime.NumGoroutine(), before)
585
+ time.Sleep(10 * time.Second) // a leaked timer or goroutine would fire here
586
+ if m.req(0).Ctx.Err() == nil {
587
+ return
588
+ }
589
+ if errors.Is(m.req(0).Ctx.Err(), context.DeadlineExceeded) {
590
+ t.Fatal("the attempt timer was still armed after the response")
591
+ }
592
+ })
593
+ }
594
+
595
+ func TestTimeouts_RejectsInvalidTimeoutsFromConfigOrPerCall(t *testing.T) {
596
+ // Adapted: zero means the default in Go; a negative timeout is the invalid case.
597
+ twin(t, "reliability.test.ts | timeouts rejects invalid timeouts from config or per call")
598
+ _, err := NewClient(Config{APIKey: "k", Getenv: noEnv, Timeout: -time.Millisecond})
599
+ mustAs[*TypeSafeError](t, err)
600
+ contains(t, err.Error(), "Timeout")
601
+ c := newClient(t, modelsOK())
602
+ _, err = c.Models().List(ctxBG(), &RequestOptions{Timeout: -5 * time.Millisecond})
603
+ mustAs[*TypeSafeError](t, err)
604
+ contains(t, err.Error(), "Timeout")
605
+ }
606
+
607
+ func TestRateLimitError_ExposesTheParsedRetryAfterInMilliseconds(t *testing.T) {
608
+ twin(t, "reliability.test.ts | RateLimitError exposes the parsed Retry-After in milliseconds")
609
+ c := clientReturning(t, func() *http.Response { return jsonResp(429, map[string]any{}, "retry-after", "7") })
610
+ _, err := c.Models().List(ctxBG(), nil)
611
+ rl := mustAs[*RateLimitError](t, err)
612
+ if !rl.HasRetryAfter || rl.RetryAfter != 7*time.Second {
613
+ t.Fatalf("got %v %v", rl.RetryAfter, rl.HasRetryAfter)
614
+ }
615
+ }
616
+
617
+ func TestRateLimitError_IsUndefinedWhenTheServerSentNoRetryAfter(t *testing.T) {
618
+ twin(t, "reliability.test.ts | RateLimitError is undefined when the server sent no Retry-After")
619
+ c := clientReturning(t, func() *http.Response { return jsonResp(429, map[string]any{}) })
620
+ _, err := c.Models().List(ctxBG(), nil)
621
+ if mustAs[*RateLimitError](t, err).HasRetryAfter {
622
+ t.Fatal("no Retry-After header, no value")
623
+ }
624
+ }
625
+
626
+ func TestBrowsers(t *testing.T) {
627
+ skipTwin(t, "there is no browser in Go: the guard and dangerouslyAllowBrowser are not ported",
628
+ "reliability.test.ts | browsers refuses to construct in a browser by default",
629
+ "reliability.test.ts | browsers constructs in a browser when explicitly allowed",
630
+ "reliability.test.ts | browsers calls the global fetch with a receiver it accepts")
631
+ }
632
+
633
+ func TestBrowsers_DoesNotMistakeNodeForABrowser(t *testing.T) {
634
+ twin(t, "reliability.test.ts | browsers does not mistake Node for a browser")
635
+ if _, err := NewClient(Config{APIKey: "k", Getenv: noEnv}); err != nil {
636
+ t.Fatal(err)
637
+ }
638
+ }
639
+
640
+ func TestBrowsers_FailsClearlyWhenNoFetchExistsAndNoneWasProvided(t *testing.T) {
641
+ // Adapted: Go always has a default HTTP client, so there is no "no global fetch" failure.
642
+ twin(t, "reliability.test.ts | browsers fails clearly when no fetch exists and none was provided")
643
+ if _, err := NewClient(Config{APIKey: "k", Getenv: noEnv}); err != nil {
644
+ t.Fatal("the default transport must always exist:", err)
645
+ }
646
+ if _, err := NewClient(Config{APIKey: "k", Getenv: noEnv, HTTPClient: modelsOK()}); err != nil {
647
+ t.Fatal(err)
648
+ }
649
+ }