@pi-in-go/pigpen-pi-typesafe 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 (189) hide show
  1. package/CREDITS.md +14 -0
  2. package/LICENSE +22 -0
  3. package/README.md +45 -0
  4. package/extensions/pi-typesafe/branches_test.go +185 -0
  5. package/extensions/pi-typesafe/command.go +319 -0
  6. package/extensions/pi-typesafe/export_test.go +9 -0
  7. package/extensions/pi-typesafe/extension.go +188 -0
  8. package/extensions/pi-typesafe/extension_test.go +321 -0
  9. package/extensions/pi-typesafe/fakehost_test.go +548 -0
  10. package/extensions/pi-typesafe/format.go +191 -0
  11. package/extensions/pi-typesafe/format_test.go +75 -0
  12. package/extensions/pi-typesafe/go.mod +10 -0
  13. package/extensions/pi-typesafe/go.sum +2 -0
  14. package/extensions/pi-typesafe/go.work +11 -0
  15. package/extensions/pi-typesafe/harness_test.go +200 -0
  16. package/extensions/pi-typesafe/ownmodel_test.go +100 -0
  17. package/extensions/pi-typesafe/review_test.go +134 -0
  18. package/extensions/pi-typesafe/tool.go +193 -0
  19. package/extensions/pi-typesafe/twin_test.go +28 -0
  20. package/libs/pi-typesafe-api/CREDITS.md +14 -0
  21. package/libs/pi-typesafe-api/LICENSE +22 -0
  22. package/libs/pi-typesafe-api/README.md +30 -0
  23. package/libs/pi-typesafe-api/ask.go +62 -0
  24. package/libs/pi-typesafe-api/ask_test.go +76 -0
  25. package/libs/pi-typesafe-api/auth.go +249 -0
  26. package/libs/pi-typesafe-api/auth_test.go +131 -0
  27. package/libs/pi-typesafe-api/backends.go +336 -0
  28. package/libs/pi-typesafe-api/backends_test.go +404 -0
  29. package/libs/pi-typesafe-api/batch.go +202 -0
  30. package/libs/pi-typesafe-api/batch_test.go +202 -0
  31. package/libs/pi-typesafe-api/battery_test.go +41 -0
  32. package/libs/pi-typesafe-api/calibrate.go +354 -0
  33. package/libs/pi-typesafe-api/calibrate_test.go +186 -0
  34. package/libs/pi-typesafe-api/client.go +615 -0
  35. package/libs/pi-typesafe-api/client_test.go +490 -0
  36. package/libs/pi-typesafe-api/credentials.go +252 -0
  37. package/libs/pi-typesafe-api/credentials_test.go +216 -0
  38. package/libs/pi-typesafe-api/doc.go +14 -0
  39. package/libs/pi-typesafe-api/errors.go +143 -0
  40. package/libs/pi-typesafe-api/evaluation.go +86 -0
  41. package/libs/pi-typesafe-api/evaluation_schema.json +264 -0
  42. package/libs/pi-typesafe-api/gaps_test.go +77 -0
  43. package/libs/pi-typesafe-api/go.mod +9 -0
  44. package/libs/pi-typesafe-api/go.sum +2 -0
  45. package/libs/pi-typesafe-api/helpers_test.go +169 -0
  46. package/libs/pi-typesafe-api/hostmodel/hostmodel.go +87 -0
  47. package/libs/pi-typesafe-api/json.go +299 -0
  48. package/libs/pi-typesafe-api/json_test.go +92 -0
  49. package/libs/pi-typesafe-api/ownmodel_test.go +79 -0
  50. package/libs/pi-typesafe-api/package.json +40 -0
  51. package/libs/pi-typesafe-api/provenance.json +18 -0
  52. package/libs/pi-typesafe-api/review_test.go +23 -0
  53. package/libs/pi-typesafe-api/schema.go +473 -0
  54. package/libs/pi-typesafe-api/schema_test.go +262 -0
  55. package/libs/pi-typesafe-api/testdata/tools/typebox-messages.mts +5 -0
  56. package/libs/pi-typesafe-api/testdata/typebox-messages.json +285 -0
  57. package/libs/pi-typesafe-api/twin_test.go +28 -0
  58. package/libs/pi-typesafe-api/ui/fakehost_test.go +548 -0
  59. package/libs/pi-typesafe-api/ui/keyprompt.go +115 -0
  60. package/libs/pi-typesafe-api/ui/login.go +106 -0
  61. package/libs/pi-typesafe-api/ui/twin_test.go +28 -0
  62. package/libs/pi-typesafe-api/ui/ui_test.go +285 -0
  63. package/libs/pi-typesafe-api/usage.go +366 -0
  64. package/libs/pi-typesafe-api/usage_test.go +139 -0
  65. package/libs/typesafe/CONTRACT.md +125 -0
  66. package/libs/typesafe/CREDITS.md +37 -0
  67. package/libs/typesafe/LICENSE +23 -0
  68. package/libs/typesafe/README.md +19 -0
  69. package/libs/typesafe/go.mod +3 -0
  70. package/libs/typesafe/libraries/ownmodel/backend_test.go +496 -0
  71. package/libs/typesafe/libraries/ownmodel/canon.go +190 -0
  72. package/libs/typesafe/libraries/ownmodel/convert.go +199 -0
  73. package/libs/typesafe/libraries/ownmodel/doc.go +15 -0
  74. package/libs/typesafe/libraries/ownmodel/equivalence_test.go +199 -0
  75. package/libs/typesafe/libraries/ownmodel/helpers_test.go +155 -0
  76. package/libs/typesafe/libraries/ownmodel/mutation_test.go +31 -0
  77. package/libs/typesafe/libraries/ownmodel/ownmodel.go +225 -0
  78. package/libs/typesafe/libraries/ownmodel/plan.go +442 -0
  79. package/libs/typesafe/libraries/ownmodel/run.go +288 -0
  80. package/libs/typesafe/libraries/ownmodel/schema_test.go +254 -0
  81. package/libs/typesafe/libraries/ownmodel/twins_test.go +169 -0
  82. package/libs/typesafe/libraries/ownmodel/utils_test.go +125 -0
  83. package/libs/typesafe/libraries/pigmodel/pigmodel.go +264 -0
  84. package/libs/typesafe/libraries/pigmodel/pigmodel_test.go +410 -0
  85. package/libs/typesafe/libraries/typesafe/answers.go +268 -0
  86. package/libs/typesafe/libraries/typesafe/api_response_test.go +113 -0
  87. package/libs/typesafe/libraries/typesafe/batch.go +80 -0
  88. package/libs/typesafe/libraries/typesafe/batch_test.go +133 -0
  89. package/libs/typesafe/libraries/typesafe/bench_test.go +71 -0
  90. package/libs/typesafe/libraries/typesafe/client.go +561 -0
  91. package/libs/typesafe/libraries/typesafe/client_test.go +495 -0
  92. package/libs/typesafe/libraries/typesafe/crosscheck_test.go +464 -0
  93. package/libs/typesafe/libraries/typesafe/crosscheck_workflowevals_test.go +219 -0
  94. package/libs/typesafe/libraries/typesafe/doc.go +27 -0
  95. package/libs/typesafe/libraries/typesafe/entry.go +142 -0
  96. package/libs/typesafe/libraries/typesafe/env.go +11 -0
  97. package/libs/typesafe/libraries/typesafe/errors.go +310 -0
  98. package/libs/typesafe/libraries/typesafe/errors_test.go +175 -0
  99. package/libs/typesafe/libraries/typesafe/helpers_test.go +294 -0
  100. package/libs/typesafe/libraries/typesafe/live_test.go +96 -0
  101. package/libs/typesafe/libraries/typesafe/logging.go +160 -0
  102. package/libs/typesafe/libraries/typesafe/logging_test.go +259 -0
  103. package/libs/typesafe/libraries/typesafe/marshal_test.go +112 -0
  104. package/libs/typesafe/libraries/typesafe/mutation_test.go +39 -0
  105. package/libs/typesafe/libraries/typesafe/questions.go +490 -0
  106. package/libs/typesafe/libraries/typesafe/questions_test.go +166 -0
  107. package/libs/typesafe/libraries/typesafe/regressions_test.go +159 -0
  108. package/libs/typesafe/libraries/typesafe/reliability_test.go +649 -0
  109. package/libs/typesafe/libraries/typesafe/retry.go +350 -0
  110. package/libs/typesafe/libraries/typesafe/retry_test.go +297 -0
  111. package/libs/typesafe/libraries/typesafe/runtime_test.go +26 -0
  112. package/libs/typesafe/libraries/typesafe/transport_test.go +163 -0
  113. package/libs/typesafe/libraries/typesafe/twins_test.go +127 -0
  114. package/libs/typesafe/libraries/typesafe/types_test.go +165 -0
  115. package/libs/typesafe/libraries/typesafe/version.go +10 -0
  116. package/libs/typesafe/package.json +37 -0
  117. package/libs/typesafe/provenance.json +49 -0
  118. package/package.json +42 -0
  119. package/port/PORT.md +98 -0
  120. package/port/accepted-gaps.json +3 -0
  121. package/port/golden/enable-confirm.jsonl +11 -0
  122. package/port/golden/enable-decline.jsonl +20 -0
  123. package/port/golden/enable-missing-key.jsonl +4 -0
  124. package/port/golden/login-shadow.jsonl +4 -0
  125. package/port/golden/logout-env-key.jsonl +6 -0
  126. package/port/golden/playground-cancel.jsonl +4 -0
  127. package/port/golden/playground-invalid-json.jsonl +5 -0
  128. package/port/golden/playground-invalid-questions.jsonl +5 -0
  129. package/port/golden/status-env-key.jsonl +6 -0
  130. package/port/golden/status-no-key.jsonl +6 -0
  131. package/port/golden/tool-disabled.jsonl +18 -0
  132. package/port/golden/trailing-words.jsonl +10 -0
  133. package/port/library-mutations.py +44 -0
  134. package/port/mutations.json +302 -0
  135. package/port/oracle/.env.example +4 -0
  136. package/port/oracle/CHANGELOG.md +91 -0
  137. package/port/oracle/CONTRIBUTING.md +35 -0
  138. package/port/oracle/LICENSE +21 -0
  139. package/port/oracle/README.md +159 -0
  140. package/port/oracle/docs/api.md +143 -0
  141. package/port/oracle/docs/ci-cd.md +97 -0
  142. package/port/oracle/examples/decision-extension.ts +41 -0
  143. package/port/oracle/extensions/index.js +2 -0
  144. package/port/oracle/package.json +89 -0
  145. package/port/oracle/scripts/dev-pi.mjs +23 -0
  146. package/port/oracle/scripts/live-smoke.mjs +35 -0
  147. package/port/oracle/src/ask.ts +42 -0
  148. package/port/oracle/src/auth.ts +171 -0
  149. package/port/oracle/src/backends.ts +196 -0
  150. package/port/oracle/src/batch.ts +170 -0
  151. package/port/oracle/src/calibrate.ts +237 -0
  152. package/port/oracle/src/client.ts +310 -0
  153. package/port/oracle/src/credentials.ts +136 -0
  154. package/port/oracle/src/errors.ts +53 -0
  155. package/port/oracle/src/extension.ts +204 -0
  156. package/port/oracle/src/index.ts +31 -0
  157. package/port/oracle/src/key-prompt.ts +51 -0
  158. package/port/oracle/src/login.ts +60 -0
  159. package/port/oracle/src/schema.ts +158 -0
  160. package/port/oracle/src/ui.ts +4 -0
  161. package/port/oracle/src/usage.ts +258 -0
  162. package/port/oracle/tests/ask.test.ts +63 -0
  163. package/port/oracle/tests/auth.test.ts +141 -0
  164. package/port/oracle/tests/backends.test.ts +380 -0
  165. package/port/oracle/tests/batch.test.ts +156 -0
  166. package/port/oracle/tests/calibrate.test.ts +144 -0
  167. package/port/oracle/tests/client.test.ts +499 -0
  168. package/port/oracle/tests/credentials.test.ts +144 -0
  169. package/port/oracle/tests/extension.test.ts +276 -0
  170. package/port/oracle/tests/key-prompt.test.ts +47 -0
  171. package/port/oracle/tests/login.test.ts +101 -0
  172. package/port/oracle/tests/schema.test.ts +85 -0
  173. package/port/oracle/tests/usage.test.ts +106 -0
  174. package/port/oracle/tsconfig.build.json +10 -0
  175. package/port/oracle/tsconfig.json +14 -0
  176. package/port/scenarios/enable-confirm.json +5 -0
  177. package/port/scenarios/enable-decline.json +3 -0
  178. package/port/scenarios/enable-missing-key.json +2 -0
  179. package/port/scenarios/login-shadow.json +2 -0
  180. package/port/scenarios/logout-env-key.json +3 -0
  181. package/port/scenarios/playground-cancel.json +2 -0
  182. package/port/scenarios/playground-invalid-json.json +2 -0
  183. package/port/scenarios/playground-invalid-questions.json +2 -0
  184. package/port/scenarios/status-env-key.json +3 -0
  185. package/port/scenarios/status-no-key.json +3 -0
  186. package/port/scenarios/tool-disabled.json +2 -0
  187. package/port/scenarios/trailing-words.json +5 -0
  188. package/port/upstream-tests.json +160 -0
  189. package/provenance.json +18 -0
@@ -0,0 +1,294 @@
1
+ package typesafe
2
+
3
+ import (
4
+ "bytes"
5
+ "context"
6
+ "encoding/json"
7
+ "errors"
8
+ "fmt"
9
+ "io"
10
+ "math"
11
+ "net/http"
12
+ "reflect"
13
+ "strings"
14
+ "sync"
15
+ "testing"
16
+ "time"
17
+ )
18
+
19
+ // twin marks a test as the twin of upstream cases (see twins_test.go): ids are
20
+ // "<file> | <full name>" from port/twins/typesafe-sdk-js.txt. Ported cases keep the
21
+ // original inputs and expectations, adapted only where the notes on the test say so.
22
+ func twin(t testing.TB, ids ...string) { t.Helper() }
23
+
24
+ // skipTwin records upstream cases with no Go counterpart, with the reason, and skips.
25
+ func skipTwin(t testing.TB, reason string, ids ...string) {
26
+ t.Helper()
27
+ t.Skip(reason)
28
+ }
29
+
30
+ // recordedRequest is what the fake transport saw.
31
+ type recordedRequest struct {
32
+ URL string
33
+ Method string
34
+ Header http.Header
35
+ Raw []byte
36
+ Body any // decoded JSON body, nil when there is none
37
+ Ctx context.Context
38
+ }
39
+
40
+ // mockDoer is the analog of the upstream mockFetch: it records requests and answers with
41
+ // whatever respond returns.
42
+ type mockDoer struct {
43
+ mu sync.Mutex
44
+ requests []recordedRequest
45
+ respond func(r *recordedRequest) (*http.Response, error)
46
+ }
47
+
48
+ func newMock(respond func(r *recordedRequest) (*http.Response, error)) *mockDoer {
49
+ return &mockDoer{respond: respond}
50
+ }
51
+
52
+ func (m *mockDoer) Do(req *http.Request) (*http.Response, error) {
53
+ rec := recordedRequest{URL: req.URL.String(), Method: req.Method, Header: req.Header.Clone(), Ctx: req.Context()}
54
+ if req.Body != nil {
55
+ rec.Raw, _ = io.ReadAll(req.Body)
56
+ if len(rec.Raw) > 0 {
57
+ _ = json.Unmarshal(rec.Raw, &rec.Body)
58
+ }
59
+ }
60
+ m.mu.Lock()
61
+ m.requests = append(m.requests, rec)
62
+ m.mu.Unlock()
63
+ return m.respond(&rec)
64
+ }
65
+
66
+ func (m *mockDoer) count() int {
67
+ m.mu.Lock()
68
+ defer m.mu.Unlock()
69
+ return len(m.requests)
70
+ }
71
+
72
+ func (m *mockDoer) req(i int) recordedRequest {
73
+ m.mu.Lock()
74
+ defer m.mu.Unlock()
75
+ return m.requests[i]
76
+ }
77
+
78
+ // jsonResp builds a JSON response; headers are name, value pairs.
79
+ func jsonResp(status int, data any, headers ...string) *http.Response {
80
+ raw, err := json.Marshal(data)
81
+ if err != nil {
82
+ panic(err)
83
+ }
84
+ return textResp(status, string(raw), append([]string{"content-type", "application/json"}, headers...)...)
85
+ }
86
+
87
+ func textResp(status int, body string, headers ...string) *http.Response {
88
+ h := http.Header{}
89
+ for i := 0; i+1 < len(headers); i += 2 {
90
+ h.Set(headers[i], headers[i+1])
91
+ }
92
+ return &http.Response{StatusCode: status, Status: fmt.Sprintf("%d %s", status, http.StatusText(status)), Header: h, Body: io.NopCloser(strings.NewReader(body)), ContentLength: int64(len(body))}
93
+ }
94
+
95
+ func always(resp func() *http.Response) *mockDoer {
96
+ return newMock(func(*recordedRequest) (*http.Response, error) { return resp(), nil })
97
+ }
98
+
99
+ // noEnv is an empty environment.
100
+ func noEnv(string) string { return "" }
101
+
102
+ func envOf(m map[string]string) func(string) string { return func(k string) string { return m[k] } }
103
+
104
+ // newClient builds a client with no environment and jitter-free delays.
105
+ func newClient(t testing.TB, doer HTTPDoer, mutate ...func(*Config)) *Client {
106
+ t.Helper()
107
+ cfg := Config{APIKey: "k", HTTPClient: doer, Getenv: noEnv}
108
+ for _, m := range mutate {
109
+ m(&cfg)
110
+ }
111
+ c, err := NewClient(cfg)
112
+ if err != nil {
113
+ t.Fatalf("NewClient: %v", err)
114
+ }
115
+ c.random = func() float64 { return 0 }
116
+ return c
117
+ }
118
+
119
+ const modelsBody = `{"models":[{"name":"m","description":"d","release_date":"2026"}]}`
120
+
121
+ var modelCards = []ModelCard{{Name: "m", Description: "d", ReleaseDate: "2026"}}
122
+
123
+ var systemOneResponse = map[string]any{
124
+ "model": "m",
125
+ "answers": map[string]any{"q1": map[string]any{"type": "noul", "noul": 0.5}},
126
+ "usage": map[string]any{"input_tokens": 1, "output_tokens": 1},
127
+ }
128
+
129
+ // jsonEq compares got (any Go value, marshalled) with want (JSON text), ignoring key order.
130
+ func jsonEq(t testing.TB, got any, want string) {
131
+ t.Helper()
132
+ raw, err := json.Marshal(got)
133
+ if err != nil {
134
+ t.Fatalf("marshal: %v", err)
135
+ }
136
+ var g, w any
137
+ if err := json.Unmarshal(raw, &g); err != nil {
138
+ t.Fatalf("got is not JSON: %s", raw)
139
+ }
140
+ if err := json.Unmarshal([]byte(want), &w); err != nil {
141
+ t.Fatalf("bad want %s: %v", want, err)
142
+ }
143
+ if !reflect.DeepEqual(g, w) {
144
+ t.Fatalf("JSON mismatch\n got: %s\nwant: %s", raw, want)
145
+ }
146
+ }
147
+
148
+ func decode(t testing.TB, s string) any {
149
+ t.Helper()
150
+ var v any
151
+ if err := json.Unmarshal([]byte(s), &v); err != nil {
152
+ t.Fatalf("bad JSON %s: %v", s, err)
153
+ }
154
+ return v
155
+ }
156
+
157
+ // recordingLogger records every call.
158
+ type logCall struct {
159
+ Level string
160
+ Message string
161
+ Args []any
162
+ }
163
+
164
+ type recordingLogger struct {
165
+ mu sync.Mutex
166
+ calls []logCall
167
+ }
168
+
169
+ func (l *recordingLogger) add(level, msg string, args []any) {
170
+ l.mu.Lock()
171
+ defer l.mu.Unlock()
172
+ l.calls = append(l.calls, logCall{level, msg, args})
173
+ }
174
+ func (l *recordingLogger) Debug(m string, a ...any) { l.add("debug", m, a) }
175
+ func (l *recordingLogger) Info(m string, a ...any) { l.add("info", m, a) }
176
+ func (l *recordingLogger) Warn(m string, a ...any) { l.add("warn", m, a) }
177
+ func (l *recordingLogger) Error(m string, a ...any) { l.add("error", m, a) }
178
+
179
+ func (l *recordingLogger) snapshot() []logCall {
180
+ l.mu.Lock()
181
+ defer l.mu.Unlock()
182
+ return append([]logCall(nil), l.calls...)
183
+ }
184
+
185
+ func (l *recordingLogger) messages(level string) []string {
186
+ var out []string
187
+ for _, c := range l.snapshot() {
188
+ if level == "" || c.Level == level {
189
+ out = append(out, c.Message)
190
+ }
191
+ }
192
+ return out
193
+ }
194
+
195
+ // hangingDoer waits for the request's context to end, like a hung fetch.
196
+ func hangingDoer() *mockDoer {
197
+ return newMock(func(r *recordedRequest) (*http.Response, error) {
198
+ <-r.Ctx.Done()
199
+ return nil, r.Ctx.Err()
200
+ })
201
+ }
202
+
203
+ // blockingBody is a response body that never yields until closed, whatever the context.
204
+ type blockingBody struct {
205
+ mu sync.Mutex
206
+ closed chan struct{}
207
+ closes int
208
+ }
209
+
210
+ func newBlockingBody() *blockingBody { return &blockingBody{closed: make(chan struct{})} }
211
+
212
+ func (b *blockingBody) Read([]byte) (int, error) {
213
+ <-b.closed
214
+ return 0, io.ErrClosedPipe
215
+ }
216
+
217
+ func (b *blockingBody) Close() error {
218
+ b.mu.Lock()
219
+ defer b.mu.Unlock()
220
+ if b.closes == 0 {
221
+ close(b.closed)
222
+ }
223
+ b.closes++
224
+ return nil
225
+ }
226
+
227
+ func (b *blockingBody) closeCount() int {
228
+ b.mu.Lock()
229
+ defer b.mu.Unlock()
230
+ return b.closes
231
+ }
232
+
233
+ // errBody fails its first read with err.
234
+ type errBody struct{ err error }
235
+
236
+ func (b errBody) Read([]byte) (int, error) { return 0, b.err }
237
+ func (b errBody) Close() error { return nil }
238
+
239
+ func respWithBody(status int, body io.ReadCloser, headers ...string) *http.Response {
240
+ r := textResp(status, "", headers...)
241
+ r.Body = body
242
+ r.ContentLength = -1
243
+ return r
244
+ }
245
+
246
+ func mustAs[T any](t testing.TB, err error) T {
247
+ t.Helper()
248
+ var target T
249
+ if !errors.As(err, &target) {
250
+ t.Fatalf("error %T (%v) is not %T", err, err, target)
251
+ }
252
+ return target
253
+ }
254
+
255
+ func notAs[T any](t testing.TB, err error) {
256
+ t.Helper()
257
+ var target T
258
+ if errors.As(err, &target) {
259
+ t.Fatalf("error %T (%v) unexpectedly is %T", err, err, target)
260
+ }
261
+ }
262
+
263
+ func contains(t testing.TB, s, sub string) {
264
+ t.Helper()
265
+ if !strings.Contains(s, sub) {
266
+ t.Fatalf("%q does not contain %q", s, sub)
267
+ }
268
+ }
269
+
270
+ func eq[T any](t testing.TB, got, want T) {
271
+ t.Helper()
272
+ if !reflect.DeepEqual(got, want) {
273
+ t.Fatalf("got %#v, want %#v", got, want)
274
+ }
275
+ }
276
+
277
+ func noErr(t testing.TB, err error) {
278
+ t.Helper()
279
+ if err != nil {
280
+ t.Fatalf("unexpected error %T: %v", err, err)
281
+ }
282
+ }
283
+
284
+ func ctxBG() context.Context { return context.Background() }
285
+
286
+ var _ = bytes.NewReader
287
+ var _ = time.Second
288
+
289
+ func fmtAny(v any) string { return fmt.Sprintf("%v %+v", v, v) }
290
+
291
+ func nan() float64 { return math.NaN() }
292
+ func inf() float64 { return math.Inf(1) }
293
+
294
+ func asErr[T any](err error, target *T) bool { return errors.As(err, target) }
@@ -0,0 +1,96 @@
1
+ package typesafe_test
2
+
3
+ import (
4
+ "bytes"
5
+ "encoding/json"
6
+ "os"
7
+ "os/exec"
8
+ "sort"
9
+ "testing"
10
+
11
+ "github.com/MichaelKinsy/pigpen/components/typesafe/libraries/typesafe"
12
+ )
13
+
14
+ // TestLive_AgainstTheRealAPI runs only when the owner supplies TYPESAFE_API_KEY; without it,
15
+ // nothing touches the network. It checks that a real response decodes into the typed
16
+ // results and, when TYPESAFE_LIVE_JS_DIST names the official SDK's dist/index.mjs and node is
17
+ // installed, that the official SDK sees the same model list and the same answer shapes.
18
+ func TestLive_AgainstTheRealAPI(t *testing.T) {
19
+ if os.Getenv("TYPESAFE_API_KEY") == "" {
20
+ t.Skip("TYPESAFE_API_KEY is not set: no live check (the fake-server cross-check runs instead)")
21
+ }
22
+ client, err := typesafe.NewClient(typesafe.Config{})
23
+ if err != nil {
24
+ t.Fatal(err)
25
+ }
26
+ models, err := client.Models().List(t.Context(), nil)
27
+ if err != nil || len(models) == 0 {
28
+ t.Fatalf("models: %v %v", models, err)
29
+ }
30
+ res, err := client.SystemOne(t.Context(), typesafe.SystemOneRequest{
31
+ State: typesafe.Text("I was charged twice for my subscription. Please refund one payment."),
32
+ Questions: typesafe.Questions{
33
+ typesafe.Ask("billing", typesafe.Noul("Is this about billing?")),
34
+ typesafe.Ask("kind", typesafe.Choice("What does the customer want?", typesafe.Opt("refund", "a refund"), typesafe.Opt("cancel", "to cancel"), typesafe.Opt("other", nil))),
35
+ typesafe.Ask("urgency", typesafe.Score("How urgent is it?", "not urgent", "somewhat urgent", "urgent")),
36
+ },
37
+ }, nil)
38
+ if err != nil {
39
+ t.Fatal(err)
40
+ }
41
+ billing, _ := res.Noul("billing")
42
+ kind, _ := res.Choice("kind")
43
+ urgency, _ := res.Score("urgency")
44
+ if billing.Noul < 0 || billing.Noul > 1 || len(kind.Probabilities) != 3 || len(urgency.Probabilities) != 3 || res.Usage.InputTokens == 0 {
45
+ t.Fatalf("unexpected result: %+v", res)
46
+ }
47
+ dist := os.Getenv("TYPESAFE_LIVE_JS_DIST")
48
+ if dist == "" {
49
+ t.Log("TYPESAFE_LIVE_JS_DIST not set: no comparison with the official SDK")
50
+ return
51
+ }
52
+ out, err := exec.CommandContext(t.Context(), "node", "../../port/crosscheck/live_js.mjs", dist).Output()
53
+ if err != nil {
54
+ t.Fatalf("live_js.mjs: %v", err)
55
+ }
56
+ var js struct {
57
+ Models []typesafe.ModelCard `json:"models"`
58
+ Result struct {
59
+ Answers map[string]struct {
60
+ Type string `json:"type"`
61
+ Probabilities map[string]float64 `json:"probabilities"`
62
+ } `json:"answers"`
63
+ } `json:"result"`
64
+ }
65
+ if err := json.NewDecoder(bytes.NewReader(out)).Decode(&js); err != nil {
66
+ t.Fatal(err)
67
+ }
68
+ names := func(ms []typesafe.ModelCard) []string {
69
+ var n []string
70
+ for _, m := range ms {
71
+ n = append(n, m.Name)
72
+ }
73
+ sort.Strings(n)
74
+ return n
75
+ }
76
+ if a, b := names(models), names(js.Models); !equalStrings(a, b) {
77
+ t.Errorf("model lists differ: go %v, official %v", a, b)
78
+ }
79
+ for name, want := range map[string]int{"kind": 3, "urgency": 3} {
80
+ if got := len(js.Result.Answers[name].Probabilities); got != want {
81
+ t.Errorf("official SDK: %s has %d probabilities, want %d", name, got, want)
82
+ }
83
+ }
84
+ }
85
+
86
+ func equalStrings(a, b []string) bool {
87
+ if len(a) != len(b) {
88
+ return false
89
+ }
90
+ for i := range a {
91
+ if a[i] != b[i] {
92
+ return false
93
+ }
94
+ }
95
+ return true
96
+ }
@@ -0,0 +1,160 @@
1
+ package typesafe
2
+
3
+ import (
4
+ "encoding/json"
5
+ "fmt"
6
+ "os"
7
+ "regexp"
8
+ "strings"
9
+ )
10
+
11
+ // LogLevel is the log verbosity; LogOff disables logging.
12
+ type LogLevel string
13
+
14
+ // Log levels from most to least verbose.
15
+ const (
16
+ LogDebug LogLevel = "debug"
17
+ LogInfo LogLevel = "info"
18
+ LogWarn LogLevel = "warn"
19
+ LogError LogLevel = "error"
20
+ LogOff LogLevel = "off"
21
+ )
22
+
23
+ // LogLevels lists the supported levels, most verbose first.
24
+ var LogLevels = []LogLevel{LogDebug, LogInfo, LogWarn, LogError, LogOff}
25
+
26
+ // DefaultLogLevel applies when neither the configuration nor the environment set one.
27
+ const DefaultLogLevel = LogWarn
28
+
29
+ // ParseLogLevel validates a level; the error names value and source (for example
30
+ // `Invalid log level "loud" from TYPESAFE_LOG_LEVEL. Expected one of: debug, info, warn, error, off.`).
31
+ func ParseLogLevel(value, source string) (LogLevel, error) {
32
+ for _, l := range LogLevels {
33
+ if string(l) == value {
34
+ return l, nil
35
+ }
36
+ }
37
+ names := make([]string, len(LogLevels))
38
+ for i, l := range LogLevels {
39
+ names[i] = string(l)
40
+ }
41
+ return "", errorf("Invalid log level %q from %s. Expected one of: %s.", value, source, strings.Join(names, ", "))
42
+ }
43
+
44
+ // Logger receives a message and structured values. The SDK never logs at warn or
45
+ // error by itself; info logs request summaries and debug adds headers and bodies.
46
+ type Logger interface {
47
+ Debug(message string, args ...any)
48
+ Info(message string, args ...any)
49
+ Warn(message string, args ...any)
50
+ Error(message string, args ...any)
51
+ }
52
+
53
+ const logPrefix = "[typesafe-sdk] "
54
+
55
+ type stderrLogger struct{}
56
+
57
+ // NewStderrLogger returns the default logger: lines prefixed "[typesafe-sdk] " on stderr.
58
+ func NewStderrLogger() Logger { return stderrLogger{} }
59
+
60
+ func (stderrLogger) write(message string, args []any) {
61
+ var b strings.Builder
62
+ b.WriteString(logPrefix)
63
+ b.WriteString(message)
64
+ for _, a := range args {
65
+ b.WriteByte(' ')
66
+ switch v := a.(type) {
67
+ case error:
68
+ b.WriteString(v.Error())
69
+ case string:
70
+ b.WriteString(v)
71
+ default:
72
+ if raw, err := json.Marshal(a); err == nil {
73
+ b.Write(raw)
74
+ } else {
75
+ fmt.Fprintf(&b, "%v", a)
76
+ }
77
+ }
78
+ }
79
+ b.WriteByte('\n')
80
+ _, _ = os.Stderr.WriteString(b.String())
81
+ }
82
+
83
+ func (l stderrLogger) Debug(m string, a ...any) { l.write(m, a) }
84
+ func (l stderrLogger) Info(m string, a ...any) { l.write(m, a) }
85
+ func (l stderrLogger) Warn(m string, a ...any) { l.write(m, a) }
86
+ func (l stderrLogger) Error(m string, a ...any) { l.write(m, a) }
87
+
88
+ var levelRank = map[LogLevel]int{LogDebug: 0, LogInfo: 1, LogWarn: 2, LogError: 3, LogOff: 4}
89
+
90
+ type levelLogger struct {
91
+ sink Logger
92
+ rank int
93
+ }
94
+
95
+ // WithLevel filters a logger to level and above.
96
+ func WithLevel(sink Logger, level LogLevel) Logger {
97
+ return levelLogger{sink: sink, rank: levelRank[level]}
98
+ }
99
+
100
+ func (l levelLogger) Debug(m string, a ...any) {
101
+ if l.rank <= levelRank[LogDebug] {
102
+ l.sink.Debug(m, a...)
103
+ }
104
+ }
105
+ func (l levelLogger) Info(m string, a ...any) {
106
+ if l.rank <= levelRank[LogInfo] {
107
+ l.sink.Info(m, a...)
108
+ }
109
+ }
110
+ func (l levelLogger) Warn(m string, a ...any) {
111
+ if l.rank <= levelRank[LogWarn] {
112
+ l.sink.Warn(m, a...)
113
+ }
114
+ }
115
+ func (l levelLogger) Error(m string, a ...any) {
116
+ if l.rank <= levelRank[LogError] {
117
+ l.sink.Error(m, a...)
118
+ }
119
+ }
120
+
121
+ var whitespaceRun = regexp.MustCompile(`\s+`)
122
+
123
+ // redactKey masks a key, preserving its scheme and the last four characters of secrets longer than eight.
124
+ func redactKey(value string) string {
125
+ scheme, secret := "", value
126
+ if strings.Contains(value, " ") {
127
+ parts := whitespaceRun.Split(value, -1) // JavaScript split(/\s+/, 2) keeps the first two parts
128
+ scheme = parts[0]
129
+ secret = ""
130
+ if len(parts) > 1 {
131
+ secret = parts[1]
132
+ }
133
+ }
134
+ tail := ""
135
+ if r := []rune(secret); len(r) > 8 {
136
+ tail = string(r[len(r)-4:])
137
+ }
138
+ if scheme != "" {
139
+ scheme += " "
140
+ }
141
+ return scheme + "***" + tail
142
+ }
143
+
144
+ // RedactHeaders returns a copy with credentials masked: authorization,
145
+ // proxy-authorization and x-api-key keep their scheme and the last four characters
146
+ // of secrets longer than eight ("Bearer ***cdef"); cookie and set-cookie become "***".
147
+ func RedactHeaders(headers map[string]string) map[string]string {
148
+ out := make(map[string]string, len(headers))
149
+ for name, value := range headers {
150
+ switch strings.ToLower(name) {
151
+ case "authorization", "proxy-authorization", "x-api-key":
152
+ out[name] = redactKey(value)
153
+ case "cookie", "set-cookie":
154
+ out[name] = "***"
155
+ default:
156
+ out[name] = value
157
+ }
158
+ }
159
+ return out
160
+ }