@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,113 @@
|
|
|
1
|
+
package typesafe
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"encoding/json"
|
|
5
|
+
"net/http"
|
|
6
|
+
"testing"
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
func clientWith(t *testing.T, resp func() *http.Response) (*Client, *mockDoer) {
|
|
10
|
+
m := always(resp)
|
|
11
|
+
return newClient(t, m, func(c *Config) { c.Retry = RetryOverrides{MaxRetries: Ptr(0)} }), m
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
func TestAPIPromise_IsReturnedFromClientMethodsAndIsARealPromise(t *testing.T) {
|
|
15
|
+
skipTwin(t, "JavaScript Promise subclass; Go returns (value, error) and the *Response types are TestAPIPromise_WithResponse*",
|
|
16
|
+
"api-promise.test.ts | APIPromise is returned from client methods and is a real Promise")
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
func TestAPIPromise_AwaitsToTheParsedData(t *testing.T) {
|
|
20
|
+
twin(t, "api-promise.test.ts | APIPromise awaits to the parsed data")
|
|
21
|
+
c, _ := clientWith(t, func() *http.Response { return textResp(200, modelsBody, "content-type", "application/json") })
|
|
22
|
+
got, err := c.Models().List(ctxBG(), nil)
|
|
23
|
+
noErr(t, err)
|
|
24
|
+
eq(t, got, modelCards)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
func TestAPIPromise_WithResponseReturnsDataResponseAndRequestID(t *testing.T) {
|
|
28
|
+
twin(t, "api-promise.test.ts | APIPromise withResponse() returns data, response, and requestId")
|
|
29
|
+
c, _ := clientWith(t, func() *http.Response {
|
|
30
|
+
return textResp(200, modelsBody, "content-type", "application/json", "x-typesafe-request-id", "req_abc")
|
|
31
|
+
})
|
|
32
|
+
res, err := c.Models().ListWithResponse(ctxBG(), nil)
|
|
33
|
+
noErr(t, err)
|
|
34
|
+
eq(t, res.Data, modelCards)
|
|
35
|
+
eq(t, res.Status, 200)
|
|
36
|
+
eq(t, res.Header.Get("x-typesafe-request-id"), "req_abc")
|
|
37
|
+
eq(t, res.RequestID, "req_abc")
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
func TestAPIPromise_WithResponseHasUndefinedRequestIDWhenTheHeaderIsAbsent(t *testing.T) {
|
|
41
|
+
twin(t, "api-promise.test.ts | APIPromise withResponse() has undefined requestId when the header is absent")
|
|
42
|
+
c, _ := clientWith(t, func() *http.Response { return textResp(200, modelsBody, "content-type", "application/json") })
|
|
43
|
+
res, err := c.Models().ListWithResponse(ctxBG(), nil)
|
|
44
|
+
noErr(t, err)
|
|
45
|
+
eq(t, res.RequestID, "")
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
func TestAPIPromise_AsResponseReturnsTheRawResponseWithAnUnconsumedBody(t *testing.T) {
|
|
49
|
+
// Adapted: the raw response carries the buffered body bytes.
|
|
50
|
+
twin(t, "api-promise.test.ts | APIPromise asResponse() returns the raw Response with an unconsumed body")
|
|
51
|
+
c, _ := clientWith(t, func() *http.Response { return textResp(200, modelsBody, "content-type", "application/json") })
|
|
52
|
+
raw, err := c.Models().ListRaw(ctxBG(), nil)
|
|
53
|
+
noErr(t, err)
|
|
54
|
+
eq(t, raw.Status, 200)
|
|
55
|
+
var got map[string]any
|
|
56
|
+
noErr(t, json.Unmarshal(raw.Body, &got))
|
|
57
|
+
eq(t, len(got["models"].([]any)), 1)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
func TestAPIPromise_ParsesTheBodyOnlyOnceAcrossMultipleConsumers(t *testing.T) {
|
|
61
|
+
skipTwin(t, "promise sharing across several consumers; a Go call returns its single result, and TestAPIPromise_OneRequestPerCall shows one request per call",
|
|
62
|
+
"api-promise.test.ts | APIPromise parses the body only once across multiple consumers")
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
func TestAPIPromise_OneRequestPerCall(t *testing.T) {
|
|
66
|
+
c, m := clientWith(t, func() *http.Response { return textResp(200, modelsBody, "content-type", "application/json") })
|
|
67
|
+
_, err := c.Models().ListWithResponse(ctxBG(), nil)
|
|
68
|
+
noErr(t, err)
|
|
69
|
+
eq(t, m.count(), 1)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
func TestAPIPromise_RejectsWithAPIErrorOnEveryPath(t *testing.T) {
|
|
73
|
+
twin(t, "api-promise.test.ts | APIPromise rejects with APIError on every path")
|
|
74
|
+
c, _ := clientWith(t, func() *http.Response { return jsonResp(404, map[string]any{"message": "nope"}) })
|
|
75
|
+
_, err := c.Models().List(ctxBG(), nil)
|
|
76
|
+
mustAs[*NotFoundError](t, err)
|
|
77
|
+
mustAs[*APIError](t, err)
|
|
78
|
+
_, err = c.Models().ListWithResponse(ctxBG(), nil)
|
|
79
|
+
mustAs[*NotFoundError](t, err)
|
|
80
|
+
_, err = c.Models().ListRaw(ctxBG(), nil)
|
|
81
|
+
mustAs[*NotFoundError](t, err)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
func TestAPIPromise_MapTransformsTheDataWhileSharingTheResponseAndParsingOnce(t *testing.T) {
|
|
85
|
+
twin(t, "api-promise.test.ts | APIPromise map() transforms the data while sharing the response and parsing once")
|
|
86
|
+
c, m := clientWith(t, func() *http.Response {
|
|
87
|
+
return textResp(200, modelsBody, "content-type", "application/json", "x-typesafe-request-id", "req_m")
|
|
88
|
+
})
|
|
89
|
+
res, err := c.Models().ListWithResponse(ctxBG(), nil)
|
|
90
|
+
noErr(t, err)
|
|
91
|
+
names := MapResponse(res, func(cards []ModelCard) []string {
|
|
92
|
+
var out []string
|
|
93
|
+
for _, c := range cards {
|
|
94
|
+
out = append(out, c.Name)
|
|
95
|
+
}
|
|
96
|
+
return out
|
|
97
|
+
})
|
|
98
|
+
eq(t, names.Data, []string{"m"})
|
|
99
|
+
eq(t, names.RequestID, "req_m")
|
|
100
|
+
eq(t, names.Status, 200)
|
|
101
|
+
eq(t, res.Data, modelCards)
|
|
102
|
+
eq(t, m.count(), 1)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
func TestAPIPromise_SupportsThenCatchFinallyChaining(t *testing.T) {
|
|
106
|
+
skipTwin(t, "JavaScript thenable chaining; Go uses ordinary error returns",
|
|
107
|
+
"api-promise.test.ts | APIPromise supports then/catch/finally chaining")
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
func TestAPIPromise_DoesNotIssueTheRequestUntilConstructedButDoesNotRequireAwaitingToSend(t *testing.T) {
|
|
111
|
+
skipTwin(t, "a Go call is synchronous: the request is sent when the method is called and there is no unawaited promise",
|
|
112
|
+
"api-promise.test.ts | APIPromise does not issue the request until constructed, but does not require awaiting to send")
|
|
113
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
package typesafe
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"sync"
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
// BatchItem is one request of a batch.
|
|
9
|
+
type BatchItem struct {
|
|
10
|
+
Request SystemOneRequest
|
|
11
|
+
Options *RequestOptions
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// BatchResult is the outcome of one item; exactly one of Result and Err is set.
|
|
15
|
+
type BatchResult struct {
|
|
16
|
+
// Index is the position of the item in the input.
|
|
17
|
+
Index int
|
|
18
|
+
Result *SystemOneResult
|
|
19
|
+
Err error
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// BatchOptions configure [EvaluateBatch].
|
|
23
|
+
type BatchOptions struct {
|
|
24
|
+
// Concurrency is the number of requests in flight; zero means DefaultBatchConcurrency.
|
|
25
|
+
Concurrency int
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// DefaultBatchConcurrency is the number of in-flight requests when none is set.
|
|
29
|
+
const DefaultBatchConcurrency = 4
|
|
30
|
+
|
|
31
|
+
// EvaluateBatch runs the items through ev with bounded concurrency and returns one
|
|
32
|
+
// result per item, in input order. An item's failure is its own Err and does not stop
|
|
33
|
+
// the others; when ctx ends, items not yet started get an *APIUserAbortError. A
|
|
34
|
+
// panic in ev becomes that item's error. The TypeScript SDK has no batch call: this
|
|
35
|
+
// is a helper added for the extensions' batched evaluate tools.
|
|
36
|
+
func EvaluateBatch(ctx context.Context, ev Evaluator, items []BatchItem, opts BatchOptions) []BatchResult {
|
|
37
|
+
results := make([]BatchResult, len(items))
|
|
38
|
+
for i := range results {
|
|
39
|
+
results[i].Index = i
|
|
40
|
+
}
|
|
41
|
+
concurrency := opts.Concurrency
|
|
42
|
+
if concurrency <= 0 {
|
|
43
|
+
concurrency = DefaultBatchConcurrency
|
|
44
|
+
}
|
|
45
|
+
sem := make(chan struct{}, concurrency)
|
|
46
|
+
var wg sync.WaitGroup
|
|
47
|
+
abortFrom := func(i int) {
|
|
48
|
+
for ; i < len(items); i++ {
|
|
49
|
+
results[i].Err = newAbortError(context.Cause(ctx))
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
loop:
|
|
53
|
+
for i := range items {
|
|
54
|
+
select {
|
|
55
|
+
case sem <- struct{}{}:
|
|
56
|
+
case <-ctx.Done():
|
|
57
|
+
abortFrom(i)
|
|
58
|
+
break loop
|
|
59
|
+
}
|
|
60
|
+
if ctx.Err() != nil {
|
|
61
|
+
<-sem
|
|
62
|
+
abortFrom(i)
|
|
63
|
+
break loop
|
|
64
|
+
}
|
|
65
|
+
wg.Add(1)
|
|
66
|
+
go func(i int) {
|
|
67
|
+
defer wg.Done()
|
|
68
|
+
defer func() { <-sem }()
|
|
69
|
+
defer func() {
|
|
70
|
+
if r := recover(); r != nil {
|
|
71
|
+
results[i].Result = nil
|
|
72
|
+
results[i].Err = errorf("The evaluator panicked: %v", r)
|
|
73
|
+
}
|
|
74
|
+
}()
|
|
75
|
+
results[i].Result, results[i].Err = ev.SystemOne(ctx, items[i].Request, items[i].Options)
|
|
76
|
+
}(i)
|
|
77
|
+
}
|
|
78
|
+
wg.Wait()
|
|
79
|
+
return results
|
|
80
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
package typesafe
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"errors"
|
|
6
|
+
"sync"
|
|
7
|
+
"sync/atomic"
|
|
8
|
+
"testing"
|
|
9
|
+
"time"
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
type evalFunc func(ctx context.Context, req SystemOneRequest, opts *RequestOptions) (*SystemOneResult, error)
|
|
13
|
+
|
|
14
|
+
func (f evalFunc) SystemOne(ctx context.Context, req SystemOneRequest, opts *RequestOptions) (*SystemOneResult, error) {
|
|
15
|
+
return f(ctx, req, opts)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
func batchItems(n int) []BatchItem {
|
|
19
|
+
items := make([]BatchItem, n)
|
|
20
|
+
for i := range items {
|
|
21
|
+
items[i] = BatchItem{Request: SystemOneRequest{Model: string(rune('a' + i)), Questions: Questions{Ask("q", Noul("?"))}}}
|
|
22
|
+
}
|
|
23
|
+
return items
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
func TestEvaluateBatch_ReturnsOneResultPerItemInInputOrder(t *testing.T) {
|
|
27
|
+
ev := evalFunc(func(ctx context.Context, req SystemOneRequest, _ *RequestOptions) (*SystemOneResult, error) {
|
|
28
|
+
// Later items finish first.
|
|
29
|
+
time.Sleep(time.Duration(10-int(req.Model[0]-'a')) * time.Millisecond)
|
|
30
|
+
return &SystemOneResult{Model: req.Model}, nil
|
|
31
|
+
})
|
|
32
|
+
out := EvaluateBatch(context.Background(), ev, batchItems(8), BatchOptions{Concurrency: 8})
|
|
33
|
+
eq(t, len(out), 8)
|
|
34
|
+
for i, r := range out {
|
|
35
|
+
eq(t, r.Index, i)
|
|
36
|
+
noErr(t, r.Err)
|
|
37
|
+
eq(t, r.Result.Model, string(rune('a'+i)))
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
func TestEvaluateBatch_BoundsConcurrency(t *testing.T) {
|
|
42
|
+
var inflight, peak atomic.Int32
|
|
43
|
+
ev := evalFunc(func(ctx context.Context, req SystemOneRequest, _ *RequestOptions) (*SystemOneResult, error) {
|
|
44
|
+
n := inflight.Add(1)
|
|
45
|
+
for {
|
|
46
|
+
p := peak.Load()
|
|
47
|
+
if n <= p || peak.CompareAndSwap(p, n) {
|
|
48
|
+
break
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
time.Sleep(5 * time.Millisecond)
|
|
52
|
+
inflight.Add(-1)
|
|
53
|
+
return &SystemOneResult{}, nil
|
|
54
|
+
})
|
|
55
|
+
EvaluateBatch(context.Background(), ev, batchItems(12), BatchOptions{Concurrency: 3})
|
|
56
|
+
if peak.Load() > 3 || peak.Load() < 2 {
|
|
57
|
+
t.Fatalf("peak concurrency %d, want 2..3", peak.Load())
|
|
58
|
+
}
|
|
59
|
+
peak.Store(0)
|
|
60
|
+
EvaluateBatch(context.Background(), ev, batchItems(20), BatchOptions{})
|
|
61
|
+
if peak.Load() > DefaultBatchConcurrency {
|
|
62
|
+
t.Fatalf("default concurrency exceeded: %d", peak.Load())
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
func TestEvaluateBatch_AnItemsFailureDoesNotStopTheOthers(t *testing.T) {
|
|
67
|
+
boom := errors.New("boom")
|
|
68
|
+
ev := evalFunc(func(ctx context.Context, req SystemOneRequest, _ *RequestOptions) (*SystemOneResult, error) {
|
|
69
|
+
if req.Model == "b" {
|
|
70
|
+
return nil, boom
|
|
71
|
+
}
|
|
72
|
+
return &SystemOneResult{Model: req.Model}, nil
|
|
73
|
+
})
|
|
74
|
+
out := EvaluateBatch(context.Background(), ev, batchItems(3), BatchOptions{Concurrency: 1})
|
|
75
|
+
noErr(t, out[0].Err)
|
|
76
|
+
if !errors.Is(out[1].Err, boom) || out[1].Result != nil {
|
|
77
|
+
t.Fatalf("item 1: %+v", out[1])
|
|
78
|
+
}
|
|
79
|
+
noErr(t, out[2].Err)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
func TestEvaluateBatch_ContextEndAbortsItemsNotYetStarted(t *testing.T) {
|
|
83
|
+
ctx, cancel := context.WithCancel(context.Background())
|
|
84
|
+
var started atomic.Int32
|
|
85
|
+
ev := evalFunc(func(ctx context.Context, req SystemOneRequest, _ *RequestOptions) (*SystemOneResult, error) {
|
|
86
|
+
if started.Add(1) == 1 {
|
|
87
|
+
cancel()
|
|
88
|
+
}
|
|
89
|
+
return &SystemOneResult{}, nil
|
|
90
|
+
})
|
|
91
|
+
out := EvaluateBatch(ctx, ev, batchItems(6), BatchOptions{Concurrency: 1})
|
|
92
|
+
noErr(t, out[0].Err)
|
|
93
|
+
for _, r := range out[1:] {
|
|
94
|
+
mustAs[*APIUserAbortError](t, r.Err)
|
|
95
|
+
if !errors.Is(r.Err, context.Canceled) {
|
|
96
|
+
t.Fatalf("abort must carry the context error: %v", r.Err)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
eq(t, int(started.Load()), 1)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
func TestEvaluateBatch_APanicBecomesTheItemsError(t *testing.T) {
|
|
103
|
+
ev := evalFunc(func(ctx context.Context, req SystemOneRequest, _ *RequestOptions) (*SystemOneResult, error) {
|
|
104
|
+
if req.Model == "a" {
|
|
105
|
+
panic("kaboom")
|
|
106
|
+
}
|
|
107
|
+
return &SystemOneResult{}, nil
|
|
108
|
+
})
|
|
109
|
+
out := EvaluateBatch(context.Background(), ev, batchItems(2), BatchOptions{})
|
|
110
|
+
if out[0].Err == nil {
|
|
111
|
+
t.Fatal("panic swallowed")
|
|
112
|
+
}
|
|
113
|
+
contains(t, out[0].Err.Error(), "kaboom")
|
|
114
|
+
noErr(t, out[1].Err)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
func TestEvaluateBatch_EmptyAndPerItemOptions(t *testing.T) {
|
|
118
|
+
eq(t, len(EvaluateBatch(context.Background(), evalFunc(nil), nil, BatchOptions{})), 0)
|
|
119
|
+
var seen sync.Map
|
|
120
|
+
ev := evalFunc(func(ctx context.Context, req SystemOneRequest, o *RequestOptions) (*SystemOneResult, error) {
|
|
121
|
+
seen.Store(req.Model, o)
|
|
122
|
+
return &SystemOneResult{}, nil
|
|
123
|
+
})
|
|
124
|
+
items := batchItems(2)
|
|
125
|
+
items[1].Options = &RequestOptions{Timeout: time.Second}
|
|
126
|
+
EvaluateBatch(context.Background(), ev, items, BatchOptions{})
|
|
127
|
+
if o, _ := seen.Load("a"); o.(*RequestOptions) != nil {
|
|
128
|
+
t.Fatal("no options for item a")
|
|
129
|
+
}
|
|
130
|
+
if o, _ := seen.Load("b"); o.(*RequestOptions).Timeout != time.Second {
|
|
131
|
+
t.Fatal("options for item b not passed")
|
|
132
|
+
}
|
|
133
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
package typesafe
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"net/http"
|
|
5
|
+
"net/http/httptest"
|
|
6
|
+
"testing"
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
// One SystemOne round trip against a local server: request build and validation, header merge, the HTTP call,
|
|
10
|
+
// response parse. The network is the dominant cost in production; these benchmarks are about what the client
|
|
11
|
+
// itself adds per call (and whether the default log level, warn, makes logging free).
|
|
12
|
+
//
|
|
13
|
+
// go test -run xxx -bench . -benchmem
|
|
14
|
+
const benchResponse = `{"model":"jev-1","answers":{"destructive":{"type":"noul","noul":0.4},"exfiltration":{"type":"noul","noul":0.1},"beyond_scope":{"type":"noul","noul":0.2},"impact":{"type":"score","score":1.2,"confidence":0.9,"probabilities":{"0":0.1,"1":0.7,"2":0.15,"3":0.05}}},"usage":{"input_tokens":120,"output_tokens":4}}`
|
|
15
|
+
|
|
16
|
+
func benchClient(b *testing.B, level LogLevel) *Client {
|
|
17
|
+
b.Helper()
|
|
18
|
+
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
19
|
+
_, _ = w.Write([]byte(benchResponse))
|
|
20
|
+
}))
|
|
21
|
+
b.Cleanup(func() { s.CloseClientConnections(); s.Close() })
|
|
22
|
+
c, err := NewClient(Config{APIKey: "test", BaseURL: s.URL, Getenv: noEnv, LogLevel: level})
|
|
23
|
+
if err != nil {
|
|
24
|
+
b.Fatal(err)
|
|
25
|
+
}
|
|
26
|
+
return c
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
func benchRequest() SystemOneRequest {
|
|
30
|
+
return SystemOneRequest{
|
|
31
|
+
State: Text(`{"cwd":"/work","tool":"bash","arguments":{"command":"go test ./..."},"platform":"linux"}`),
|
|
32
|
+
Questions: Questions{
|
|
33
|
+
Ask("destructive", Noul("Is this action destructive?")),
|
|
34
|
+
Ask("exfiltration", Noul("Does this action send local data to a network destination?")),
|
|
35
|
+
Ask("beyond_scope", Noul("Does this action affect anything beyond what was asked?")),
|
|
36
|
+
},
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
func BenchmarkSystemOneLogOff(b *testing.B) {
|
|
41
|
+
c, req := benchClient(b, LogOff), benchRequest()
|
|
42
|
+
b.ReportAllocs()
|
|
43
|
+
b.ResetTimer()
|
|
44
|
+
for i := 0; i < b.N; i++ {
|
|
45
|
+
if _, err := c.SystemOne(ctxBG(), req, nil); err != nil {
|
|
46
|
+
b.Fatal(err)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
func BenchmarkSystemOneLogWarn(b *testing.B) {
|
|
52
|
+
c, req := benchClient(b, LogWarn), benchRequest()
|
|
53
|
+
b.ReportAllocs()
|
|
54
|
+
b.ResetTimer()
|
|
55
|
+
for i := 0; i < b.N; i++ {
|
|
56
|
+
if _, err := c.SystemOne(ctxBG(), req, nil); err != nil {
|
|
57
|
+
b.Fatal(err)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
func BenchmarkSystemOnePayload(b *testing.B) {
|
|
63
|
+
c, req := benchClient(b, LogOff), benchRequest()
|
|
64
|
+
b.ReportAllocs()
|
|
65
|
+
b.ResetTimer()
|
|
66
|
+
for i := 0; i < b.N; i++ {
|
|
67
|
+
if _, err := c.systemOnePayload(req); err != nil {
|
|
68
|
+
b.Fatal(err)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|