@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.
- package/CREDITS.md +14 -0
- package/LICENSE +22 -0
- package/README.md +45 -0
- package/extensions/pi-typesafe/branches_test.go +185 -0
- package/extensions/pi-typesafe/command.go +319 -0
- package/extensions/pi-typesafe/export_test.go +9 -0
- package/extensions/pi-typesafe/extension.go +188 -0
- package/extensions/pi-typesafe/extension_test.go +321 -0
- package/extensions/pi-typesafe/fakehost_test.go +548 -0
- package/extensions/pi-typesafe/format.go +191 -0
- package/extensions/pi-typesafe/format_test.go +75 -0
- package/extensions/pi-typesafe/go.mod +10 -0
- package/extensions/pi-typesafe/go.sum +2 -0
- package/extensions/pi-typesafe/go.work +11 -0
- package/extensions/pi-typesafe/harness_test.go +200 -0
- package/extensions/pi-typesafe/ownmodel_test.go +100 -0
- package/extensions/pi-typesafe/review_test.go +134 -0
- package/extensions/pi-typesafe/tool.go +193 -0
- package/extensions/pi-typesafe/twin_test.go +28 -0
- package/libs/pi-typesafe-api/CREDITS.md +14 -0
- package/libs/pi-typesafe-api/LICENSE +22 -0
- package/libs/pi-typesafe-api/README.md +30 -0
- package/libs/pi-typesafe-api/ask.go +62 -0
- package/libs/pi-typesafe-api/ask_test.go +76 -0
- package/libs/pi-typesafe-api/auth.go +249 -0
- package/libs/pi-typesafe-api/auth_test.go +131 -0
- package/libs/pi-typesafe-api/backends.go +336 -0
- package/libs/pi-typesafe-api/backends_test.go +404 -0
- package/libs/pi-typesafe-api/batch.go +202 -0
- package/libs/pi-typesafe-api/batch_test.go +202 -0
- package/libs/pi-typesafe-api/battery_test.go +41 -0
- package/libs/pi-typesafe-api/calibrate.go +354 -0
- package/libs/pi-typesafe-api/calibrate_test.go +186 -0
- package/libs/pi-typesafe-api/client.go +615 -0
- package/libs/pi-typesafe-api/client_test.go +490 -0
- package/libs/pi-typesafe-api/credentials.go +252 -0
- package/libs/pi-typesafe-api/credentials_test.go +216 -0
- package/libs/pi-typesafe-api/doc.go +14 -0
- package/libs/pi-typesafe-api/errors.go +143 -0
- package/libs/pi-typesafe-api/evaluation.go +86 -0
- package/libs/pi-typesafe-api/evaluation_schema.json +264 -0
- package/libs/pi-typesafe-api/gaps_test.go +77 -0
- package/libs/pi-typesafe-api/go.mod +9 -0
- package/libs/pi-typesafe-api/go.sum +2 -0
- package/libs/pi-typesafe-api/helpers_test.go +169 -0
- package/libs/pi-typesafe-api/hostmodel/hostmodel.go +87 -0
- package/libs/pi-typesafe-api/json.go +299 -0
- package/libs/pi-typesafe-api/json_test.go +92 -0
- package/libs/pi-typesafe-api/ownmodel_test.go +79 -0
- package/libs/pi-typesafe-api/package.json +40 -0
- package/libs/pi-typesafe-api/provenance.json +18 -0
- package/libs/pi-typesafe-api/review_test.go +23 -0
- package/libs/pi-typesafe-api/schema.go +473 -0
- package/libs/pi-typesafe-api/schema_test.go +262 -0
- package/libs/pi-typesafe-api/testdata/tools/typebox-messages.mts +5 -0
- package/libs/pi-typesafe-api/testdata/typebox-messages.json +285 -0
- package/libs/pi-typesafe-api/twin_test.go +28 -0
- package/libs/pi-typesafe-api/ui/fakehost_test.go +548 -0
- package/libs/pi-typesafe-api/ui/keyprompt.go +115 -0
- package/libs/pi-typesafe-api/ui/login.go +106 -0
- package/libs/pi-typesafe-api/ui/twin_test.go +28 -0
- package/libs/pi-typesafe-api/ui/ui_test.go +285 -0
- package/libs/pi-typesafe-api/usage.go +366 -0
- package/libs/pi-typesafe-api/usage_test.go +139 -0
- package/libs/typesafe/CONTRACT.md +125 -0
- package/libs/typesafe/CREDITS.md +37 -0
- package/libs/typesafe/LICENSE +23 -0
- package/libs/typesafe/README.md +19 -0
- package/libs/typesafe/go.mod +3 -0
- package/libs/typesafe/libraries/ownmodel/backend_test.go +496 -0
- package/libs/typesafe/libraries/ownmodel/canon.go +190 -0
- package/libs/typesafe/libraries/ownmodel/convert.go +199 -0
- package/libs/typesafe/libraries/ownmodel/doc.go +15 -0
- package/libs/typesafe/libraries/ownmodel/equivalence_test.go +199 -0
- package/libs/typesafe/libraries/ownmodel/helpers_test.go +155 -0
- package/libs/typesafe/libraries/ownmodel/mutation_test.go +31 -0
- package/libs/typesafe/libraries/ownmodel/ownmodel.go +225 -0
- package/libs/typesafe/libraries/ownmodel/plan.go +442 -0
- package/libs/typesafe/libraries/ownmodel/run.go +288 -0
- package/libs/typesafe/libraries/ownmodel/schema_test.go +254 -0
- package/libs/typesafe/libraries/ownmodel/twins_test.go +169 -0
- package/libs/typesafe/libraries/ownmodel/utils_test.go +125 -0
- package/libs/typesafe/libraries/pigmodel/pigmodel.go +264 -0
- package/libs/typesafe/libraries/pigmodel/pigmodel_test.go +410 -0
- package/libs/typesafe/libraries/typesafe/answers.go +268 -0
- package/libs/typesafe/libraries/typesafe/api_response_test.go +113 -0
- package/libs/typesafe/libraries/typesafe/batch.go +80 -0
- package/libs/typesafe/libraries/typesafe/batch_test.go +133 -0
- package/libs/typesafe/libraries/typesafe/bench_test.go +71 -0
- package/libs/typesafe/libraries/typesafe/client.go +561 -0
- package/libs/typesafe/libraries/typesafe/client_test.go +495 -0
- package/libs/typesafe/libraries/typesafe/crosscheck_test.go +464 -0
- package/libs/typesafe/libraries/typesafe/crosscheck_workflowevals_test.go +219 -0
- package/libs/typesafe/libraries/typesafe/doc.go +27 -0
- package/libs/typesafe/libraries/typesafe/entry.go +142 -0
- package/libs/typesafe/libraries/typesafe/env.go +11 -0
- package/libs/typesafe/libraries/typesafe/errors.go +310 -0
- package/libs/typesafe/libraries/typesafe/errors_test.go +175 -0
- package/libs/typesafe/libraries/typesafe/helpers_test.go +294 -0
- package/libs/typesafe/libraries/typesafe/live_test.go +96 -0
- package/libs/typesafe/libraries/typesafe/logging.go +160 -0
- package/libs/typesafe/libraries/typesafe/logging_test.go +259 -0
- package/libs/typesafe/libraries/typesafe/marshal_test.go +112 -0
- package/libs/typesafe/libraries/typesafe/mutation_test.go +39 -0
- package/libs/typesafe/libraries/typesafe/questions.go +490 -0
- package/libs/typesafe/libraries/typesafe/questions_test.go +166 -0
- package/libs/typesafe/libraries/typesafe/regressions_test.go +159 -0
- package/libs/typesafe/libraries/typesafe/reliability_test.go +649 -0
- package/libs/typesafe/libraries/typesafe/retry.go +350 -0
- package/libs/typesafe/libraries/typesafe/retry_test.go +297 -0
- package/libs/typesafe/libraries/typesafe/runtime_test.go +26 -0
- package/libs/typesafe/libraries/typesafe/transport_test.go +163 -0
- package/libs/typesafe/libraries/typesafe/twins_test.go +127 -0
- package/libs/typesafe/libraries/typesafe/types_test.go +165 -0
- package/libs/typesafe/libraries/typesafe/version.go +10 -0
- package/libs/typesafe/package.json +37 -0
- package/libs/typesafe/provenance.json +49 -0
- package/package.json +42 -0
- package/port/PORT.md +98 -0
- package/port/accepted-gaps.json +3 -0
- package/port/golden/enable-confirm.jsonl +11 -0
- package/port/golden/enable-decline.jsonl +20 -0
- package/port/golden/enable-missing-key.jsonl +4 -0
- package/port/golden/login-shadow.jsonl +4 -0
- package/port/golden/logout-env-key.jsonl +6 -0
- package/port/golden/playground-cancel.jsonl +4 -0
- package/port/golden/playground-invalid-json.jsonl +5 -0
- package/port/golden/playground-invalid-questions.jsonl +5 -0
- package/port/golden/status-env-key.jsonl +6 -0
- package/port/golden/status-no-key.jsonl +6 -0
- package/port/golden/tool-disabled.jsonl +18 -0
- package/port/golden/trailing-words.jsonl +10 -0
- package/port/library-mutations.py +44 -0
- package/port/mutations.json +302 -0
- package/port/oracle/.env.example +4 -0
- package/port/oracle/CHANGELOG.md +91 -0
- package/port/oracle/CONTRIBUTING.md +35 -0
- package/port/oracle/LICENSE +21 -0
- package/port/oracle/README.md +159 -0
- package/port/oracle/docs/api.md +143 -0
- package/port/oracle/docs/ci-cd.md +97 -0
- package/port/oracle/examples/decision-extension.ts +41 -0
- package/port/oracle/extensions/index.js +2 -0
- package/port/oracle/package.json +89 -0
- package/port/oracle/scripts/dev-pi.mjs +23 -0
- package/port/oracle/scripts/live-smoke.mjs +35 -0
- package/port/oracle/src/ask.ts +42 -0
- package/port/oracle/src/auth.ts +171 -0
- package/port/oracle/src/backends.ts +196 -0
- package/port/oracle/src/batch.ts +170 -0
- package/port/oracle/src/calibrate.ts +237 -0
- package/port/oracle/src/client.ts +310 -0
- package/port/oracle/src/credentials.ts +136 -0
- package/port/oracle/src/errors.ts +53 -0
- package/port/oracle/src/extension.ts +204 -0
- package/port/oracle/src/index.ts +31 -0
- package/port/oracle/src/key-prompt.ts +51 -0
- package/port/oracle/src/login.ts +60 -0
- package/port/oracle/src/schema.ts +158 -0
- package/port/oracle/src/ui.ts +4 -0
- package/port/oracle/src/usage.ts +258 -0
- package/port/oracle/tests/ask.test.ts +63 -0
- package/port/oracle/tests/auth.test.ts +141 -0
- package/port/oracle/tests/backends.test.ts +380 -0
- package/port/oracle/tests/batch.test.ts +156 -0
- package/port/oracle/tests/calibrate.test.ts +144 -0
- package/port/oracle/tests/client.test.ts +499 -0
- package/port/oracle/tests/credentials.test.ts +144 -0
- package/port/oracle/tests/extension.test.ts +276 -0
- package/port/oracle/tests/key-prompt.test.ts +47 -0
- package/port/oracle/tests/login.test.ts +101 -0
- package/port/oracle/tests/schema.test.ts +85 -0
- package/port/oracle/tests/usage.test.ts +106 -0
- package/port/oracle/tsconfig.build.json +10 -0
- package/port/oracle/tsconfig.json +14 -0
- package/port/scenarios/enable-confirm.json +5 -0
- package/port/scenarios/enable-decline.json +3 -0
- package/port/scenarios/enable-missing-key.json +2 -0
- package/port/scenarios/login-shadow.json +2 -0
- package/port/scenarios/logout-env-key.json +3 -0
- package/port/scenarios/playground-cancel.json +2 -0
- package/port/scenarios/playground-invalid-json.json +2 -0
- package/port/scenarios/playground-invalid-questions.json +2 -0
- package/port/scenarios/status-env-key.json +3 -0
- package/port/scenarios/status-no-key.json +3 -0
- package/port/scenarios/tool-disabled.json +2 -0
- package/port/scenarios/trailing-words.json +5 -0
- package/port/upstream-tests.json +160 -0
- package/provenance.json +18 -0
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
package typesafe_test
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"encoding/json"
|
|
5
|
+
"errors"
|
|
6
|
+
"fmt"
|
|
7
|
+
"io"
|
|
8
|
+
"net"
|
|
9
|
+
"net/http"
|
|
10
|
+
"net/http/httptest"
|
|
11
|
+
"os"
|
|
12
|
+
"reflect"
|
|
13
|
+
"regexp"
|
|
14
|
+
"sort"
|
|
15
|
+
"strings"
|
|
16
|
+
"sync"
|
|
17
|
+
"testing"
|
|
18
|
+
"time"
|
|
19
|
+
|
|
20
|
+
"github.com/MichaelKinsy/pigpen/components/typesafe/libraries/typesafe"
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
// Cross-check against the official SDK. port/crosscheck/record_js.mjs runs every scenario in
|
|
24
|
+
// scenarios.json through @typesafe-ai/sdk 0.6.0 (built from the pinned commit) against a
|
|
25
|
+
// scripted local server and records the requests the server saw and the outcome. This test
|
|
26
|
+
// replays the same scenarios through the Go client against an equivalent scripted server
|
|
27
|
+
// and compares request shape (method, path, headers, body) and outcome (result or error
|
|
28
|
+
// class, message, status, request ID, body).
|
|
29
|
+
|
|
30
|
+
type step struct {
|
|
31
|
+
Status int `json:"status"`
|
|
32
|
+
Headers map[string]string `json:"headers"`
|
|
33
|
+
Body json.RawMessage `json:"body"`
|
|
34
|
+
BodyText *string `json:"bodyText"`
|
|
35
|
+
DelayMs int `json:"delayMs"`
|
|
36
|
+
Action string `json:"action"`
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
type scenario struct {
|
|
40
|
+
Name string `json:"name"`
|
|
41
|
+
Note string `json:"note"`
|
|
42
|
+
Config struct {
|
|
43
|
+
Retry map[string]any `json:"retry"`
|
|
44
|
+
TimeoutMs int `json:"timeoutMs"`
|
|
45
|
+
DefaultHdrs map[string]string `json:"defaultHeaders"`
|
|
46
|
+
BaseURLSuffix string `json:"baseURLSuffix"`
|
|
47
|
+
} `json:"config"`
|
|
48
|
+
Call struct {
|
|
49
|
+
Kind string `json:"kind"`
|
|
50
|
+
Request json.RawMessage `json:"request"`
|
|
51
|
+
Options struct {
|
|
52
|
+
Headers map[string]string `json:"headers"`
|
|
53
|
+
Retry map[string]any `json:"retry"`
|
|
54
|
+
TimeoutMs int `json:"timeoutMs"`
|
|
55
|
+
} `json:"options"`
|
|
56
|
+
} `json:"call"`
|
|
57
|
+
Responses []step `json:"responses"`
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
type seenRequest struct {
|
|
61
|
+
Method string `json:"method"`
|
|
62
|
+
Path string `json:"path"`
|
|
63
|
+
Headers map[string]string `json:"headers"`
|
|
64
|
+
Body string `json:"body"`
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
type outcome struct {
|
|
68
|
+
OK bool `json:"ok"`
|
|
69
|
+
Result json.RawMessage `json:"result"`
|
|
70
|
+
Class string `json:"class"`
|
|
71
|
+
Message string `json:"message"`
|
|
72
|
+
Status int `json:"status"`
|
|
73
|
+
RequestID *string `json:"requestId"`
|
|
74
|
+
Body json.RawMessage `json:"body"`
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
type golden struct {
|
|
78
|
+
Requests []seenRequest `json:"requests"`
|
|
79
|
+
Outcome outcome `json:"outcome"`
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
func readJSON(t *testing.T, path string, into any) {
|
|
83
|
+
t.Helper()
|
|
84
|
+
raw, err := os.ReadFile(path)
|
|
85
|
+
if err != nil {
|
|
86
|
+
t.Fatal(err)
|
|
87
|
+
}
|
|
88
|
+
if err := json.Unmarshal(raw, into); err != nil {
|
|
89
|
+
t.Fatalf("%s: %v", path, err)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
func retryOverrides(t *testing.T, m map[string]any) typesafe.RetryOverrides {
|
|
94
|
+
t.Helper()
|
|
95
|
+
var o typesafe.RetryOverrides
|
|
96
|
+
ms := func(v any) *time.Duration { return typesafe.Ptr(time.Duration(v.(float64)) * time.Millisecond) }
|
|
97
|
+
for k, v := range m {
|
|
98
|
+
switch k {
|
|
99
|
+
case "maxRetries":
|
|
100
|
+
o.MaxRetries = typesafe.Ptr(int(v.(float64)))
|
|
101
|
+
case "backoffInitialMs":
|
|
102
|
+
o.BackoffInitial = ms(v)
|
|
103
|
+
case "backoffMaxMs":
|
|
104
|
+
o.BackoffMax = ms(v)
|
|
105
|
+
case "backoffJitter":
|
|
106
|
+
o.BackoffJitter = typesafe.Ptr(v.(float64))
|
|
107
|
+
case "httpStatuses":
|
|
108
|
+
o.HTTPStatuses = []int{}
|
|
109
|
+
for _, s := range v.([]any) {
|
|
110
|
+
o.HTTPStatuses = append(o.HTTPStatuses, int(s.(float64)))
|
|
111
|
+
}
|
|
112
|
+
case "apiConnectionError":
|
|
113
|
+
o.APIConnectionError = typesafe.Ptr(v.(bool))
|
|
114
|
+
case "apiTimeoutError":
|
|
115
|
+
o.APITimeoutError = typesafe.Ptr(v.(bool))
|
|
116
|
+
default:
|
|
117
|
+
t.Fatalf("unmapped retry option %q", k)
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return o
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
func buildRequest(t *testing.T, raw json.RawMessage) typesafe.SystemOneRequest {
|
|
124
|
+
t.Helper()
|
|
125
|
+
var fields map[string]json.RawMessage
|
|
126
|
+
if err := json.Unmarshal(raw, &fields); err != nil {
|
|
127
|
+
t.Fatal(err)
|
|
128
|
+
}
|
|
129
|
+
var req typesafe.SystemOneRequest
|
|
130
|
+
if s, ok := fields["state"]; ok {
|
|
131
|
+
if err := json.Unmarshal(s, &req.State); err != nil {
|
|
132
|
+
t.Fatal(err)
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if q, ok := fields["questions"]; ok {
|
|
136
|
+
qs, err := typesafe.ParseQuestions(q)
|
|
137
|
+
if err != nil {
|
|
138
|
+
// A client-side rejection of the wire form (for example a score map): reported as
|
|
139
|
+
// the call's outcome, like the SDK's validation.
|
|
140
|
+
t.Logf("ParseQuestions: %v", err)
|
|
141
|
+
req.Questions = nil
|
|
142
|
+
parseErr = err
|
|
143
|
+
} else {
|
|
144
|
+
req.Questions = qs
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if m, ok := fields["model"]; ok {
|
|
148
|
+
_ = json.Unmarshal(m, &req.Model)
|
|
149
|
+
}
|
|
150
|
+
for k, v := range fields {
|
|
151
|
+
if k == "state" || k == "questions" || k == "model" {
|
|
152
|
+
continue
|
|
153
|
+
}
|
|
154
|
+
if req.Extra == nil {
|
|
155
|
+
req.Extra = map[string]any{}
|
|
156
|
+
}
|
|
157
|
+
var x any
|
|
158
|
+
if err := json.Unmarshal(v, &x); err != nil {
|
|
159
|
+
t.Fatal(err)
|
|
160
|
+
}
|
|
161
|
+
req.Extra[k] = x
|
|
162
|
+
}
|
|
163
|
+
return req
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// parseErr carries a ParseQuestions rejection to the outcome (single-goroutine use per scenario).
|
|
167
|
+
var parseErr error
|
|
168
|
+
|
|
169
|
+
// scriptedServer answers requests from steps, like record_js.mjs.
|
|
170
|
+
type scriptedServer struct {
|
|
171
|
+
srv *httptest.Server
|
|
172
|
+
mu sync.Mutex
|
|
173
|
+
seen []seenRequest
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
var keptHeader = func(n string) bool {
|
|
177
|
+
return strings.HasPrefix(n, "x-") || n == "authorization" || n == "accept" || n == "content-type" || n == "content-length"
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
func (s *scriptedServer) snapshot() []seenRequest {
|
|
181
|
+
s.srv.CloseClientConnections()
|
|
182
|
+
s.mu.Lock()
|
|
183
|
+
defer s.mu.Unlock()
|
|
184
|
+
return append([]seenRequest(nil), s.seen...)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
func newScripted(steps []step) *scriptedServer {
|
|
188
|
+
s := &scriptedServer{}
|
|
189
|
+
s.srv = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
190
|
+
body, _ := io.ReadAll(r.Body)
|
|
191
|
+
h := map[string]string{}
|
|
192
|
+
for k, v := range r.Header {
|
|
193
|
+
if n := strings.ToLower(k); keptHeader(n) {
|
|
194
|
+
h[n] = strings.Join(v, ", ")
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
if r.ContentLength >= 0 && len(body) > 0 {
|
|
198
|
+
h["content-length"] = fmt.Sprint(len(body))
|
|
199
|
+
}
|
|
200
|
+
s.mu.Lock()
|
|
201
|
+
s.seen = append(s.seen, seenRequest{Method: r.Method, Path: r.URL.RequestURI(), Headers: h, Body: string(body)})
|
|
202
|
+
n := len(s.seen)
|
|
203
|
+
s.mu.Unlock()
|
|
204
|
+
st := step{Status: 599}
|
|
205
|
+
text := "unscripted request"
|
|
206
|
+
st.BodyText = &text
|
|
207
|
+
if n <= len(steps) {
|
|
208
|
+
st = steps[n-1]
|
|
209
|
+
}
|
|
210
|
+
if st.DelayMs > 0 {
|
|
211
|
+
select {
|
|
212
|
+
case <-time.After(time.Duration(st.DelayMs) * time.Millisecond):
|
|
213
|
+
case <-r.Context().Done():
|
|
214
|
+
return
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
if st.Action == "destroy" {
|
|
218
|
+
conn, _, _ := w.(http.Hijacker).Hijack()
|
|
219
|
+
if tc, ok := conn.(*net.TCPConn); ok {
|
|
220
|
+
_ = tc.SetLinger(0)
|
|
221
|
+
}
|
|
222
|
+
_ = conn.Close()
|
|
223
|
+
return
|
|
224
|
+
}
|
|
225
|
+
for k, v := range st.Headers {
|
|
226
|
+
w.Header().Set(k, v)
|
|
227
|
+
}
|
|
228
|
+
var out []byte
|
|
229
|
+
switch {
|
|
230
|
+
case st.BodyText != nil:
|
|
231
|
+
out = []byte(*st.BodyText)
|
|
232
|
+
case st.Body != nil:
|
|
233
|
+
out = st.Body
|
|
234
|
+
w.Header().Set("content-type", "application/json")
|
|
235
|
+
}
|
|
236
|
+
w.WriteHeader(st.Status)
|
|
237
|
+
_, _ = w.Write(out)
|
|
238
|
+
}))
|
|
239
|
+
return s
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
func generic(t *testing.T, raw []byte) any {
|
|
243
|
+
t.Helper()
|
|
244
|
+
if len(raw) == 0 {
|
|
245
|
+
return nil
|
|
246
|
+
}
|
|
247
|
+
var v any
|
|
248
|
+
if err := json.Unmarshal(raw, &v); err != nil {
|
|
249
|
+
t.Fatalf("not JSON: %s", raw)
|
|
250
|
+
}
|
|
251
|
+
return v
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
func className(err error) string {
|
|
255
|
+
t := reflect.TypeOf(err)
|
|
256
|
+
for t.Kind() == reflect.Pointer {
|
|
257
|
+
t = t.Elem()
|
|
258
|
+
}
|
|
259
|
+
return t.Name()
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// knownDifferences are scenarios where the Go client deliberately differs; the test asserts
|
|
263
|
+
// that the difference exists (so a stale entry fails) and what it is.
|
|
264
|
+
var knownDifferences = map[string]string{
|
|
265
|
+
"200-invalid-json": "the TS SDK returns the raw text as the result; the Go client decodes into typed answers and reports a *TypeSafeError",
|
|
266
|
+
"200-empty-body": "the TS SDK returns undefined; the Go client returns a zero result",
|
|
267
|
+
"unknown-answer-type-and-extra-usage": "unknown fields inside a known answer, inside usage and at the top level of the result are dropped by the typed Go result; SystemOneWithResponse and SystemOneRaw keep the complete body",
|
|
268
|
+
"score-map-criteria-client-side-error": "the wire form of a score map is rejected by ParseQuestions (a Go-only entry point) with a different message; Go's builders cannot express a map",
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
var runtimeHeader = regexp.MustCompile(`^go/\S+ \(\w+; \w+\)$`)
|
|
272
|
+
|
|
273
|
+
func TestCrossCheck_ClientMatchesTheOfficialSDK(t *testing.T) {
|
|
274
|
+
var scenarios []scenario
|
|
275
|
+
readJSON(t, "../../port/crosscheck/scenarios.json", &scenarios)
|
|
276
|
+
var goldens map[string]golden
|
|
277
|
+
readJSON(t, "../../port/crosscheck/golden.json", &goldens)
|
|
278
|
+
if len(scenarios) != len(goldens) {
|
|
279
|
+
t.Fatalf("%d scenarios but %d goldens: re-record with record_js.mjs", len(scenarios), len(goldens))
|
|
280
|
+
}
|
|
281
|
+
for _, sc := range scenarios {
|
|
282
|
+
t.Run(sc.Name, func(t *testing.T) {
|
|
283
|
+
want, ok := goldens[sc.Name]
|
|
284
|
+
if !ok {
|
|
285
|
+
t.Fatal("no golden")
|
|
286
|
+
}
|
|
287
|
+
parseErr = nil
|
|
288
|
+
srv := newScripted(sc.Responses)
|
|
289
|
+
defer srv.srv.Close()
|
|
290
|
+
cfg := typesafe.Config{
|
|
291
|
+
APIKey: "test-key-not-real", BaseURL: srv.srv.URL + sc.Config.BaseURLSuffix, LogLevel: typesafe.LogOff,
|
|
292
|
+
Retry: retryOverrides(t, sc.Config.Retry), Timeout: time.Duration(sc.Config.TimeoutMs) * time.Millisecond,
|
|
293
|
+
DefaultHeaders: sc.Config.DefaultHdrs, Getenv: func(string) string { return "" },
|
|
294
|
+
}
|
|
295
|
+
client, err := typesafe.NewClient(cfg)
|
|
296
|
+
if err != nil {
|
|
297
|
+
t.Fatal(err)
|
|
298
|
+
}
|
|
299
|
+
opts := &typesafe.RequestOptions{
|
|
300
|
+
Headers: sc.Call.Options.Headers, Retry: retryOverrides(t, sc.Call.Options.Retry),
|
|
301
|
+
Timeout: time.Duration(sc.Call.Options.TimeoutMs) * time.Millisecond,
|
|
302
|
+
}
|
|
303
|
+
var got outcome
|
|
304
|
+
ctx := t.Context()
|
|
305
|
+
var res any
|
|
306
|
+
var callErr error
|
|
307
|
+
if sc.Call.Kind == "modelsList" {
|
|
308
|
+
res, callErr = client.Models().List(ctx, opts)
|
|
309
|
+
} else {
|
|
310
|
+
req := buildRequest(t, sc.Call.Request)
|
|
311
|
+
if parseErr != nil {
|
|
312
|
+
callErr = parseErr
|
|
313
|
+
} else {
|
|
314
|
+
res, callErr = client.SystemOne(ctx, req, opts)
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
if callErr == nil {
|
|
318
|
+
raw, err := json.Marshal(res)
|
|
319
|
+
if err != nil {
|
|
320
|
+
t.Fatal(err)
|
|
321
|
+
}
|
|
322
|
+
got = outcome{OK: true, Result: raw}
|
|
323
|
+
} else {
|
|
324
|
+
got = outcome{Class: className(callErr), Message: callErr.Error()}
|
|
325
|
+
var api *typesafe.APIError
|
|
326
|
+
if errors.As(callErr, &api) {
|
|
327
|
+
got.Status = api.Status
|
|
328
|
+
id := api.RequestID
|
|
329
|
+
if id != "" {
|
|
330
|
+
got.RequestID = &id
|
|
331
|
+
}
|
|
332
|
+
if api.Body != nil {
|
|
333
|
+
b, _ := json.Marshal(api.Body)
|
|
334
|
+
got.Body = b
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
compareRequests(t, want.Requests, srv.snapshot())
|
|
339
|
+
diffs := compareOutcome(t, want.Outcome, got)
|
|
340
|
+
if reason, known := knownDifferences[sc.Name]; known {
|
|
341
|
+
if len(diffs) == 0 {
|
|
342
|
+
t.Fatalf("listed as a known difference (%s) but the outcomes are equal", reason)
|
|
343
|
+
}
|
|
344
|
+
t.Logf("known difference: %s: %v", reason, diffs)
|
|
345
|
+
} else if len(diffs) > 0 {
|
|
346
|
+
t.Errorf("outcome differs from the official SDK:\n %s\n official: %+v\n go: %+v", strings.Join(diffs, "\n "), want.Outcome, got)
|
|
347
|
+
}
|
|
348
|
+
})
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
func compareRequests(t *testing.T, want, got []seenRequest) {
|
|
353
|
+
t.Helper()
|
|
354
|
+
if len(want) != len(got) {
|
|
355
|
+
t.Errorf("request count: official %d, go %d", len(want), len(got))
|
|
356
|
+
return
|
|
357
|
+
}
|
|
358
|
+
for i := range want {
|
|
359
|
+
w, g := want[i], got[i]
|
|
360
|
+
tag := fmt.Sprintf("request %d", i)
|
|
361
|
+
if w.Method != g.Method || w.Path != g.Path {
|
|
362
|
+
t.Errorf("%s: official %s %s, go %s %s", tag, w.Method, w.Path, g.Method, g.Path)
|
|
363
|
+
}
|
|
364
|
+
if !reflect.DeepEqual(generic(t, []byte(w.Body)), generic(t, []byte(g.Body))) {
|
|
365
|
+
t.Errorf("%s body differs:\n official %s\n go %s", tag, w.Body, g.Body)
|
|
366
|
+
}
|
|
367
|
+
if (w.Body == "") != (g.Body == "") {
|
|
368
|
+
t.Errorf("%s: body presence differs", tag)
|
|
369
|
+
}
|
|
370
|
+
// Key order of the fixed fields: state, questions, model.
|
|
371
|
+
if w.Body != "" && orderOfFixedKeys(w.Body) != orderOfFixedKeys(g.Body) {
|
|
372
|
+
t.Errorf("%s: field order differs: official %s, go %s", tag, orderOfFixedKeys(w.Body), orderOfFixedKeys(g.Body))
|
|
373
|
+
}
|
|
374
|
+
names := map[string]bool{}
|
|
375
|
+
for n := range w.Headers {
|
|
376
|
+
names[n] = true
|
|
377
|
+
}
|
|
378
|
+
for n := range g.Headers {
|
|
379
|
+
names[n] = true
|
|
380
|
+
}
|
|
381
|
+
var sorted []string
|
|
382
|
+
for n := range names {
|
|
383
|
+
sorted = append(sorted, n)
|
|
384
|
+
}
|
|
385
|
+
sort.Strings(sorted)
|
|
386
|
+
for _, n := range sorted {
|
|
387
|
+
wv, wok := w.Headers[n]
|
|
388
|
+
gv, gok := g.Headers[n]
|
|
389
|
+
switch n {
|
|
390
|
+
case "x-typesafe-sdk":
|
|
391
|
+
if !gok || gv != "typesafe-sdk-go/0.6.0" {
|
|
392
|
+
t.Errorf("%s: x-typesafe-sdk = %q, want typesafe-sdk-go/0.6.0 (official %q)", tag, gv, wv)
|
|
393
|
+
}
|
|
394
|
+
case "x-typesafe-runtime":
|
|
395
|
+
if !gok || !runtimeHeader.MatchString(gv) {
|
|
396
|
+
t.Errorf("%s: x-typesafe-runtime = %q, want go/<v> (<os>; <arch>) (official %q)", tag, gv, wv)
|
|
397
|
+
}
|
|
398
|
+
case "content-length":
|
|
399
|
+
// encoding/json always escapes U+2028 and U+2029 (\u2028: 6 bytes instead of 3).
|
|
400
|
+
n := strings.Count(w.Body, "\u2028") + strings.Count(w.Body, "\u2029")
|
|
401
|
+
if wv2 := fmt.Sprint(len(w.Body) + 3*n); wv2 != gv {
|
|
402
|
+
t.Errorf("%s: content-length official %q (expected %s with escapes), go %q", tag, wv, wv2, gv)
|
|
403
|
+
}
|
|
404
|
+
default:
|
|
405
|
+
if wok != gok || wv != gv {
|
|
406
|
+
t.Errorf("%s: header %s: official %q (present %v), go %q (present %v)", tag, n, wv, wok, gv, gok)
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// orderOfFixedKeys returns the order of the top-level state/questions/model keys in a body.
|
|
414
|
+
func orderOfFixedKeys(body string) string {
|
|
415
|
+
dec := json.NewDecoder(strings.NewReader(body))
|
|
416
|
+
var order []string
|
|
417
|
+
if _, err := dec.Token(); err != nil {
|
|
418
|
+
return ""
|
|
419
|
+
}
|
|
420
|
+
for dec.More() {
|
|
421
|
+
k, _ := dec.Token()
|
|
422
|
+
key := k.(string)
|
|
423
|
+
var skip json.RawMessage
|
|
424
|
+
_ = dec.Decode(&skip)
|
|
425
|
+
if key == "state" || key == "questions" || key == "model" {
|
|
426
|
+
order = append(order, key)
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
return strings.Join(order, ",")
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
func compareOutcome(t *testing.T, want, got outcome) []string {
|
|
433
|
+
t.Helper()
|
|
434
|
+
var diffs []string
|
|
435
|
+
if want.OK != got.OK {
|
|
436
|
+
return []string{fmt.Sprintf("ok: official %v, go %v", want.OK, got.OK)}
|
|
437
|
+
}
|
|
438
|
+
if want.OK {
|
|
439
|
+
if !reflect.DeepEqual(generic(t, want.Result), generic(t, got.Result)) {
|
|
440
|
+
diffs = append(diffs, fmt.Sprintf("result: official %s, go %s", want.Result, got.Result))
|
|
441
|
+
}
|
|
442
|
+
return diffs
|
|
443
|
+
}
|
|
444
|
+
if want.Class != got.Class {
|
|
445
|
+
diffs = append(diffs, fmt.Sprintf("class: official %s, go %s", want.Class, got.Class))
|
|
446
|
+
}
|
|
447
|
+
wm, gm := want.Message, got.Message
|
|
448
|
+
if want.Class == "APIConnectionError" { // the cause text belongs to the runtime's HTTP stack
|
|
449
|
+
wm, gm = strings.SplitN(wm, ":", 2)[0], strings.SplitN(gm, ":", 2)[0]
|
|
450
|
+
}
|
|
451
|
+
if wm != gm {
|
|
452
|
+
diffs = append(diffs, fmt.Sprintf("message: official %q, go %q", want.Message, got.Message))
|
|
453
|
+
}
|
|
454
|
+
if want.Status != got.Status {
|
|
455
|
+
diffs = append(diffs, fmt.Sprintf("status: official %d, go %d", want.Status, got.Status))
|
|
456
|
+
}
|
|
457
|
+
if (want.RequestID == nil) != (got.RequestID == nil) || (want.RequestID != nil && *want.RequestID != *got.RequestID) {
|
|
458
|
+
diffs = append(diffs, "request id differs")
|
|
459
|
+
}
|
|
460
|
+
if !reflect.DeepEqual(generic(t, want.Body), generic(t, got.Body)) {
|
|
461
|
+
diffs = append(diffs, fmt.Sprintf("body: official %s, go %s", want.Body, got.Body))
|
|
462
|
+
}
|
|
463
|
+
return diffs
|
|
464
|
+
}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
package typesafe_test
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"bytes"
|
|
5
|
+
"crypto/sha256"
|
|
6
|
+
"encoding/hex"
|
|
7
|
+
"encoding/json"
|
|
8
|
+
"fmt"
|
|
9
|
+
"os"
|
|
10
|
+
"sort"
|
|
11
|
+
"strconv"
|
|
12
|
+
"strings"
|
|
13
|
+
"testing"
|
|
14
|
+
|
|
15
|
+
"github.com/MichaelKinsy/pigpen/components/typesafe/libraries/typesafe"
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
// WorkflowEvals cross-check (typesafe-ai/WorkflowEvals, Apache-2.0, pinned in PORT.md).
|
|
19
|
+
// extract_workflowevals.py runs the four workflows' own eval adapters and records every
|
|
20
|
+
// system_one call: the real question sets, and the request body the Python SDK builds.
|
|
21
|
+
// record_js.mjs sends the same inputs through the official TS SDK and records a hash of the
|
|
22
|
+
// canonical body. Here the Go client sends the same inputs to a fake server and its body must
|
|
23
|
+
// equal both, whatever the key order or escaping.
|
|
24
|
+
|
|
25
|
+
type weCase struct {
|
|
26
|
+
Workflow string `json:"workflow"`
|
|
27
|
+
CaseID string `json:"case_id"`
|
|
28
|
+
Node string `json:"node"`
|
|
29
|
+
State json.RawMessage `json:"state"`
|
|
30
|
+
Questions json.RawMessage `json:"questions"`
|
|
31
|
+
PythonBody string `json:"python_body"`
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
type weGolden struct {
|
|
35
|
+
SHA256 string `json:"sha256"`
|
|
36
|
+
Order string `json:"order"`
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// canon renders JSON with sorted keys, compact, and non-ASCII escaped, like record_js.mjs.
|
|
40
|
+
func canon(t testing.TB, raw []byte) string {
|
|
41
|
+
t.Helper()
|
|
42
|
+
dec := json.NewDecoder(bytes.NewReader(raw))
|
|
43
|
+
dec.UseNumber()
|
|
44
|
+
var v any
|
|
45
|
+
if err := dec.Decode(&v); err != nil {
|
|
46
|
+
t.Fatal(err)
|
|
47
|
+
}
|
|
48
|
+
var b strings.Builder
|
|
49
|
+
canonWrite(&b, v)
|
|
50
|
+
return b.String()
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
func canonString(b *strings.Builder, s string) {
|
|
54
|
+
b.WriteByte('"')
|
|
55
|
+
for _, r := range s {
|
|
56
|
+
switch {
|
|
57
|
+
case r == '"':
|
|
58
|
+
b.WriteString(`\"`)
|
|
59
|
+
case r == '\\':
|
|
60
|
+
b.WriteString(`\\`)
|
|
61
|
+
case r == '\n':
|
|
62
|
+
b.WriteString(`\n`)
|
|
63
|
+
case r == '\r':
|
|
64
|
+
b.WriteString(`\r`)
|
|
65
|
+
case r == '\t':
|
|
66
|
+
b.WriteString(`\t`)
|
|
67
|
+
case r == '\b':
|
|
68
|
+
b.WriteString(`\b`)
|
|
69
|
+
case r == '\f':
|
|
70
|
+
b.WriteString(`\f`)
|
|
71
|
+
case r < 0x20 || (r >= 0x7f && r <= 0xffff):
|
|
72
|
+
fmt.Fprintf(b, `\u%04x`, r)
|
|
73
|
+
case r > 0xffff:
|
|
74
|
+
r -= 0x10000
|
|
75
|
+
fmt.Fprintf(b, `\u%04x\u%04x`, 0xd800+(r>>10), 0xdc00+(r&0x3ff))
|
|
76
|
+
default:
|
|
77
|
+
b.WriteRune(r)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
b.WriteByte('"')
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
func canonWrite(b *strings.Builder, v any) {
|
|
84
|
+
switch x := v.(type) {
|
|
85
|
+
case map[string]any:
|
|
86
|
+
keys := make([]string, 0, len(x))
|
|
87
|
+
for k := range x {
|
|
88
|
+
keys = append(keys, k)
|
|
89
|
+
}
|
|
90
|
+
sort.Strings(keys)
|
|
91
|
+
b.WriteByte('{')
|
|
92
|
+
for i, k := range keys {
|
|
93
|
+
if i > 0 {
|
|
94
|
+
b.WriteByte(',')
|
|
95
|
+
}
|
|
96
|
+
canonString(b, k)
|
|
97
|
+
b.WriteByte(':')
|
|
98
|
+
canonWrite(b, x[k])
|
|
99
|
+
}
|
|
100
|
+
b.WriteByte('}')
|
|
101
|
+
case []any:
|
|
102
|
+
b.WriteByte('[')
|
|
103
|
+
for i, e := range x {
|
|
104
|
+
if i > 0 {
|
|
105
|
+
b.WriteByte(',')
|
|
106
|
+
}
|
|
107
|
+
canonWrite(b, e)
|
|
108
|
+
}
|
|
109
|
+
b.WriteByte(']')
|
|
110
|
+
case string:
|
|
111
|
+
canonString(b, x)
|
|
112
|
+
case json.Number:
|
|
113
|
+
if f, err := x.Float64(); err == nil && strings.ContainsAny(x.String(), ".eE") {
|
|
114
|
+
b.WriteString(strconv.FormatFloat(f, 'f', -1, 64))
|
|
115
|
+
} else {
|
|
116
|
+
b.WriteString(x.String())
|
|
117
|
+
}
|
|
118
|
+
case bool:
|
|
119
|
+
b.WriteString(strconv.FormatBool(x))
|
|
120
|
+
case nil:
|
|
121
|
+
b.WriteString("null")
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
func keyOrder(body string) string {
|
|
126
|
+
dec := json.NewDecoder(strings.NewReader(body))
|
|
127
|
+
var keys []string
|
|
128
|
+
if _, err := dec.Token(); err != nil {
|
|
129
|
+
return ""
|
|
130
|
+
}
|
|
131
|
+
for dec.More() {
|
|
132
|
+
k, _ := dec.Token()
|
|
133
|
+
keys = append(keys, k.(string))
|
|
134
|
+
var skip json.RawMessage
|
|
135
|
+
_ = dec.Decode(&skip)
|
|
136
|
+
}
|
|
137
|
+
return strings.Join(keys, ",")
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// goBody returns the body the Go client sends for a WorkflowEvals call.
|
|
141
|
+
func goBody(t *testing.T, c weCase) string {
|
|
142
|
+
t.Helper()
|
|
143
|
+
srv := newScripted([]step{{Status: 200, Body: json.RawMessage(`{"model":"m","answers":{},"usage":{"input_tokens":0,"output_tokens":0}}`)}})
|
|
144
|
+
defer srv.srv.Close()
|
|
145
|
+
client, err := typesafe.NewClient(typesafe.Config{APIKey: "test-key-not-real", BaseURL: srv.srv.URL, LogLevel: typesafe.LogOff,
|
|
146
|
+
Retry: typesafe.RetryOverrides{MaxRetries: typesafe.Ptr(0)}, Getenv: func(string) string { return "" }})
|
|
147
|
+
if err != nil {
|
|
148
|
+
t.Fatal(err)
|
|
149
|
+
}
|
|
150
|
+
qs, err := typesafe.ParseQuestions(c.Questions)
|
|
151
|
+
if err != nil {
|
|
152
|
+
t.Fatalf("ParseQuestions: %v", err)
|
|
153
|
+
}
|
|
154
|
+
var state typesafe.Entry
|
|
155
|
+
if err := json.Unmarshal(c.State, &state); err != nil {
|
|
156
|
+
t.Fatal(err)
|
|
157
|
+
}
|
|
158
|
+
if _, err := client.SystemOne(t.Context(), typesafe.SystemOneRequest{State: state, Questions: qs, Model: "m"}, nil); err != nil {
|
|
159
|
+
t.Fatalf("SystemOne: %v", err)
|
|
160
|
+
}
|
|
161
|
+
seen := srv.snapshot()
|
|
162
|
+
if len(seen) != 1 {
|
|
163
|
+
t.Fatalf("%d requests", len(seen))
|
|
164
|
+
}
|
|
165
|
+
return seen[0].Body
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
func TestCrossCheck_WorkflowEvalsRequestsMatchTheOfficialSDKAndThePythonSDK(t *testing.T) {
|
|
169
|
+
var cases []weCase
|
|
170
|
+
readJSON(t, "../../port/crosscheck/workflowevals.json", &cases)
|
|
171
|
+
var goldens []weGolden
|
|
172
|
+
readJSON(t, "../../port/crosscheck/golden_workflowevals.json", &goldens)
|
|
173
|
+
if len(cases) != len(goldens) || len(cases) == 0 {
|
|
174
|
+
t.Fatalf("%d cases, %d goldens: re-extract and re-record", len(cases), len(goldens))
|
|
175
|
+
}
|
|
176
|
+
kinds := map[string]int{}
|
|
177
|
+
for i, c := range cases {
|
|
178
|
+
t.Run(fmt.Sprintf("%s-%s-%d", c.Workflow, c.Node, i), func(t *testing.T) {
|
|
179
|
+
body := goBody(t, c)
|
|
180
|
+
sum := sha256.Sum256([]byte(canon(t, []byte(body))))
|
|
181
|
+
if got := hex.EncodeToString(sum[:]); got != goldens[i].SHA256 {
|
|
182
|
+
t.Errorf("Go body differs from the official SDK's:\n go %s\n python %s", canon(t, []byte(body)), canon(t, []byte(c.PythonBody)))
|
|
183
|
+
}
|
|
184
|
+
if canon(t, []byte(body)) != canon(t, []byte(c.PythonBody)) {
|
|
185
|
+
t.Errorf("Go body differs from the Python SDK's body")
|
|
186
|
+
}
|
|
187
|
+
if got := keyOrder(body); got != goldens[i].Order {
|
|
188
|
+
t.Errorf("key order: Go %s, official %s", got, goldens[i].Order)
|
|
189
|
+
}
|
|
190
|
+
var qs map[string]struct {
|
|
191
|
+
Type string `json:"type"`
|
|
192
|
+
}
|
|
193
|
+
_ = json.Unmarshal(c.Questions, &qs)
|
|
194
|
+
for _, q := range qs {
|
|
195
|
+
kinds[q.Type]++
|
|
196
|
+
}
|
|
197
|
+
})
|
|
198
|
+
}
|
|
199
|
+
t.Logf("%d calls; question types seen: %v", len(cases), kinds)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// WORKFLOWEVALS_FULL names a file written by extract_workflowevals.py's second argument: real
|
|
203
|
+
// dataset states (not committed). The Go body must equal the Python SDK's body for them too.
|
|
204
|
+
func TestCrossCheck_WorkflowEvalsRealStatesMatchThePythonSDK(t *testing.T) {
|
|
205
|
+
path := os.Getenv("WORKFLOWEVALS_FULL")
|
|
206
|
+
if path == "" {
|
|
207
|
+
t.Skip("WORKFLOWEVALS_FULL is not set (the real dataset states are not committed)")
|
|
208
|
+
}
|
|
209
|
+
var cases []weCase
|
|
210
|
+
readJSON(t, path, &cases)
|
|
211
|
+
for i, c := range cases {
|
|
212
|
+
t.Run(fmt.Sprintf("%s-%s-%d", c.Workflow, c.Node, i), func(t *testing.T) {
|
|
213
|
+
body := goBody(t, c)
|
|
214
|
+
if canon(t, []byte(body)) != canon(t, []byte(c.PythonBody)) {
|
|
215
|
+
t.Errorf("Go body differs from the Python SDK's body")
|
|
216
|
+
}
|
|
217
|
+
})
|
|
218
|
+
}
|
|
219
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// Package typesafe is a Go client for the TypeSafe AI API: a port of the official
|
|
2
|
+
// TypeScript SDK @typesafe-ai/sdk 0.6.0 (https://github.com/typesafe-ai/typesafe-sdk-js,
|
|
3
|
+
// MIT; see the component's CREDITS.md and port/PORT.md for the pinned commit and the
|
|
4
|
+
// file-by-file mapping).
|
|
5
|
+
//
|
|
6
|
+
// A request carries a state (text, a JSON object or array, or null) and named typed
|
|
7
|
+
// questions. Each answer is typed by the question that produced it:
|
|
8
|
+
//
|
|
9
|
+
// client, err := typesafe.NewClient(typesafe.Config{}) // TYPESAFE_API_KEY from the environment
|
|
10
|
+
// res, err := client.SystemOne(ctx, typesafe.SystemOneRequest{
|
|
11
|
+
// State: typesafe.Text("I was charged twice. Please fix this ASAP."),
|
|
12
|
+
// Questions: typesafe.Questions{
|
|
13
|
+
// typesafe.Ask("category", typesafe.Choice("What is this ticket about?",
|
|
14
|
+
// typesafe.Opt("billing", nil), typesafe.Opt("technical", nil), typesafe.Opt("other", nil))),
|
|
15
|
+
// },
|
|
16
|
+
// }, nil)
|
|
17
|
+
// answer, err := res.Choice("category") // ChoiceAnswer{Choice, Confidence, Probabilities}
|
|
18
|
+
//
|
|
19
|
+
// The three question kinds are Noul (yes/no), Choice (named alternatives) and Score
|
|
20
|
+
// (an ordered rubric). The [Evaluator] interface is what code that only needs answers
|
|
21
|
+
// depends on; [Client] (the TypeSafe API) and the own-model backend in package
|
|
22
|
+
// ownmodel both implement it. [EvaluateBatch] runs many requests through an Evaluator
|
|
23
|
+
// with bounded concurrency.
|
|
24
|
+
//
|
|
25
|
+
// The package uses the standard library only. See CONTRACT.md in the component root
|
|
26
|
+
// for the deliberate differences from the TypeScript SDK.
|
|
27
|
+
package typesafe
|