@pi-in-go/pigpen-jev 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. package/CREDITS.md +22 -0
  2. package/LICENSE +22 -0
  3. package/README.md +237 -0
  4. package/extensions/jev/ask.go +166 -0
  5. package/extensions/jev/ask_test.go +218 -0
  6. package/extensions/jev/backend.go +128 -0
  7. package/extensions/jev/bench_test.go +64 -0
  8. package/extensions/jev/boundaries_test.go +159 -0
  9. package/extensions/jev/command.go +224 -0
  10. package/extensions/jev/commands_test.go +214 -0
  11. package/extensions/jev/config.go +450 -0
  12. package/extensions/jev/errors_test.go +191 -0
  13. package/extensions/jev/extension.go +391 -0
  14. package/extensions/jev/fakehost_test.go +548 -0
  15. package/extensions/jev/gate.go +125 -0
  16. package/extensions/jev/gate_test.go +610 -0
  17. package/extensions/jev/gatekey_test.go +24 -0
  18. package/extensions/jev/go.mod +9 -0
  19. package/extensions/jev/go.sum +2 -0
  20. package/extensions/jev/go.work +10 -0
  21. package/extensions/jev/helpers_test.go +404 -0
  22. package/extensions/jev/memo.go +88 -0
  23. package/extensions/jev/output.go +89 -0
  24. package/extensions/jev/output_test.go +187 -0
  25. package/extensions/jev/ownmodel_test.go +118 -0
  26. package/extensions/jev/render.go +136 -0
  27. package/extensions/jev/review_test.go +310 -0
  28. package/extensions/jev/source_test.go +57 -0
  29. package/extensions/jev/text.go +174 -0
  30. package/extensions/jev/trust_test.go +335 -0
  31. package/extensions/jev/types.go +227 -0
  32. package/libs/typesafe/CONTRACT.md +125 -0
  33. package/libs/typesafe/CREDITS.md +37 -0
  34. package/libs/typesafe/LICENSE +23 -0
  35. package/libs/typesafe/README.md +19 -0
  36. package/libs/typesafe/go.mod +3 -0
  37. package/libs/typesafe/libraries/ownmodel/backend_test.go +496 -0
  38. package/libs/typesafe/libraries/ownmodel/canon.go +190 -0
  39. package/libs/typesafe/libraries/ownmodel/convert.go +199 -0
  40. package/libs/typesafe/libraries/ownmodel/doc.go +15 -0
  41. package/libs/typesafe/libraries/ownmodel/equivalence_test.go +199 -0
  42. package/libs/typesafe/libraries/ownmodel/helpers_test.go +155 -0
  43. package/libs/typesafe/libraries/ownmodel/mutation_test.go +31 -0
  44. package/libs/typesafe/libraries/ownmodel/ownmodel.go +225 -0
  45. package/libs/typesafe/libraries/ownmodel/plan.go +442 -0
  46. package/libs/typesafe/libraries/ownmodel/run.go +288 -0
  47. package/libs/typesafe/libraries/ownmodel/schema_test.go +254 -0
  48. package/libs/typesafe/libraries/ownmodel/twins_test.go +169 -0
  49. package/libs/typesafe/libraries/ownmodel/utils_test.go +125 -0
  50. package/libs/typesafe/libraries/pigmodel/pigmodel.go +264 -0
  51. package/libs/typesafe/libraries/pigmodel/pigmodel_test.go +410 -0
  52. package/libs/typesafe/libraries/typesafe/answers.go +268 -0
  53. package/libs/typesafe/libraries/typesafe/api_response_test.go +113 -0
  54. package/libs/typesafe/libraries/typesafe/batch.go +80 -0
  55. package/libs/typesafe/libraries/typesafe/batch_test.go +133 -0
  56. package/libs/typesafe/libraries/typesafe/bench_test.go +71 -0
  57. package/libs/typesafe/libraries/typesafe/client.go +561 -0
  58. package/libs/typesafe/libraries/typesafe/client_test.go +495 -0
  59. package/libs/typesafe/libraries/typesafe/crosscheck_test.go +464 -0
  60. package/libs/typesafe/libraries/typesafe/crosscheck_workflowevals_test.go +219 -0
  61. package/libs/typesafe/libraries/typesafe/doc.go +27 -0
  62. package/libs/typesafe/libraries/typesafe/entry.go +142 -0
  63. package/libs/typesafe/libraries/typesafe/env.go +11 -0
  64. package/libs/typesafe/libraries/typesafe/errors.go +310 -0
  65. package/libs/typesafe/libraries/typesafe/errors_test.go +175 -0
  66. package/libs/typesafe/libraries/typesafe/helpers_test.go +294 -0
  67. package/libs/typesafe/libraries/typesafe/live_test.go +96 -0
  68. package/libs/typesafe/libraries/typesafe/logging.go +160 -0
  69. package/libs/typesafe/libraries/typesafe/logging_test.go +259 -0
  70. package/libs/typesafe/libraries/typesafe/marshal_test.go +112 -0
  71. package/libs/typesafe/libraries/typesafe/mutation_test.go +39 -0
  72. package/libs/typesafe/libraries/typesafe/questions.go +490 -0
  73. package/libs/typesafe/libraries/typesafe/questions_test.go +166 -0
  74. package/libs/typesafe/libraries/typesafe/regressions_test.go +159 -0
  75. package/libs/typesafe/libraries/typesafe/reliability_test.go +649 -0
  76. package/libs/typesafe/libraries/typesafe/retry.go +350 -0
  77. package/libs/typesafe/libraries/typesafe/retry_test.go +297 -0
  78. package/libs/typesafe/libraries/typesafe/runtime_test.go +26 -0
  79. package/libs/typesafe/libraries/typesafe/transport_test.go +163 -0
  80. package/libs/typesafe/libraries/typesafe/twins_test.go +127 -0
  81. package/libs/typesafe/libraries/typesafe/types_test.go +165 -0
  82. package/libs/typesafe/libraries/typesafe/version.go +10 -0
  83. package/libs/typesafe/package.json +37 -0
  84. package/libs/typesafe/provenance.json +49 -0
  85. package/package.json +42 -0
  86. package/port/PORT.md +107 -0
  87. package/port/e2e/gate-and-output.py +35 -0
  88. package/port/e2e/jev-ask.py +36 -0
  89. package/port/e2e/model-switch.py +44 -0
  90. package/port/e2e/off-by-default.py +34 -0
  91. package/port/gen-scenarios.py +103 -0
  92. package/port/golden/cache-identical-calls.jsonl +30 -0
  93. package/port/golden/clear.jsonl +22 -0
  94. package/port/golden/commands.jsonl +43 -0
  95. package/port/golden/enforce-accept.jsonl +23 -0
  96. package/port/golden/enforce-decline.jsonl +22 -0
  97. package/port/golden/jev-ask.jsonl +20 -0
  98. package/port/golden/output-advice.jsonl +23 -0
  99. package/port/golden/output-leak.jsonl +24 -0
  100. package/port/golden/output-low-confidence.jsonl +22 -0
  101. package/port/golden/shadow-flagged.jsonl +23 -0
  102. package/port/golden/unjudged-tools.jsonl +19 -0
  103. package/port/golden/write-elision.jsonl +21 -0
  104. package/port/mutate-unit.py +63 -0
  105. package/port/mutations.json +578 -0
  106. package/port/oracle/LICENSE +21 -0
  107. package/port/oracle/README.md +181 -0
  108. package/port/oracle/SHA256SUMS +8 -0
  109. package/port/oracle/package.json +43 -0
  110. package/port/oracle/src/client.ts +409 -0
  111. package/port/oracle/src/config.ts +363 -0
  112. package/port/oracle/src/gate.ts +229 -0
  113. package/port/oracle/src/index.ts +649 -0
  114. package/port/oracle/src/output.ts +163 -0
  115. package/port/red-run.log +309 -0
  116. package/port/scenarios/cache-identical-calls.json +71 -0
  117. package/port/scenarios/clear.json +61 -0
  118. package/port/scenarios/commands.json +119 -0
  119. package/port/scenarios/enforce-accept.json +66 -0
  120. package/port/scenarios/enforce-decline.json +57 -0
  121. package/port/scenarios/jev-ask.json +83 -0
  122. package/port/scenarios/output-advice.json +61 -0
  123. package/port/scenarios/output-leak.json +61 -0
  124. package/port/scenarios/output-low-confidence.json +61 -0
  125. package/port/scenarios/shadow-flagged.json +61 -0
  126. package/port/scenarios/unjudged-tools.json +55 -0
  127. package/port/scenarios/write-elision.json +53 -0
  128. package/provenance.json +18 -0
@@ -0,0 +1,259 @@
1
+ package typesafe
2
+
3
+ import (
4
+ "context"
5
+ "errors"
6
+ "io"
7
+ "net/http"
8
+ "os"
9
+ "regexp"
10
+ "strings"
11
+ "sync"
12
+ "testing"
13
+ )
14
+
15
+ // captureStderr runs fn and returns what it wrote to os.Stderr.
16
+ func captureStderr(t *testing.T, fn func()) string {
17
+ t.Helper()
18
+ r, w, err := os.Pipe()
19
+ noErr(t, err)
20
+ old := os.Stderr
21
+ os.Stderr = w
22
+ var out []byte
23
+ var wg sync.WaitGroup
24
+ wg.Add(1)
25
+ go func() { defer wg.Done(); out, _ = io.ReadAll(r) }()
26
+ func() {
27
+ defer func() { os.Stderr = old; _ = w.Close() }()
28
+ fn()
29
+ }()
30
+ wg.Wait()
31
+ return string(out)
32
+ }
33
+
34
+ func modelsOK() *mockDoer {
35
+ return always(func() *http.Response { return jsonResp(200, map[string]any{"models": []any{}}) })
36
+ }
37
+
38
+ func TestDefaultConsoleLogger_IsSilentAtTheDefaultLevelForASuccessfulRequest(t *testing.T) {
39
+ twin(t, "logging.test.ts | default console logger is silent at the default level for a successful request")
40
+ out := captureStderr(t, func() {
41
+ c, err := NewClient(Config{APIKey: "k", HTTPClient: modelsOK(), Getenv: noEnv})
42
+ noErr(t, err)
43
+ _, err = c.Models().List(ctxBG(), nil)
44
+ noErr(t, err)
45
+ })
46
+ eq(t, out, "")
47
+ }
48
+
49
+ func TestDefaultConsoleLogger_WritesPrefixedLinesToConsoleAtDebugLevel(t *testing.T) {
50
+ twin(t, "logging.test.ts | default console logger writes prefixed lines to console at debug level")
51
+ out := captureStderr(t, func() {
52
+ c, err := NewClient(Config{APIKey: "k", HTTPClient: modelsOK(), LogLevel: LogDebug, Getenv: noEnv})
53
+ noErr(t, err)
54
+ _, err = c.Models().List(ctxBG(), nil)
55
+ noErr(t, err)
56
+ })
57
+ lines := strings.Split(strings.TrimRight(out, "\n"), "\n")
58
+ if len(lines) < 2 {
59
+ t.Fatalf("want request and response lines, got %q", out)
60
+ }
61
+ re := regexp.MustCompile(`^\[typesafe-sdk\] #1 GET /v1/models`)
62
+ for _, l := range lines {
63
+ if !re.MatchString(l) {
64
+ t.Fatalf("unprefixed line %q", l)
65
+ }
66
+ }
67
+ }
68
+
69
+ func TestDefaultConsoleLogger_RoutesEachLevelToTheMatchingConsoleMethod(t *testing.T) {
70
+ // Adapted: Go has one stream (stderr); the message keeps the prefix and the values follow it.
71
+ twin(t, "logging.test.ts | default console logger routes each level to the matching console method")
72
+ out := captureStderr(t, func() { NewStderrLogger().Warn("careful", map[string]any{"a": 1}) })
73
+ eq(t, out, "[typesafe-sdk] careful {\"a\":1}\n")
74
+ out = captureStderr(t, func() { NewStderrLogger().Error("bad") })
75
+ eq(t, out, "[typesafe-sdk] bad\n")
76
+ }
77
+
78
+ func loggedClient(t *testing.T, logger Logger, level LogLevel) *Client {
79
+ return newClient(t, always(func() *http.Response {
80
+ return jsonResp(200, map[string]any{"models": []any{}}, "x-typesafe-request-id", "req_9")
81
+ }), func(c *Config) {
82
+ c.APIKey = "sk_live_0123456789abcdef"
83
+ c.BaseURL = "https://x.test"
84
+ c.Logger = logger
85
+ c.LogLevel = level
86
+ })
87
+ }
88
+
89
+ func TestCustomLogger_ExposesTheLevelFilteredLoggerOnTheClient(t *testing.T) {
90
+ twin(t, "logging.test.ts | custom logger and level filtering exposes the level-filtered logger on the client")
91
+ l := &recordingLogger{}
92
+ c := loggedClient(t, l, LogWarn)
93
+ c.Logger().Info("dropped")
94
+ c.Logger().Warn("kept")
95
+ eq(t, l.messages(""), []string{"kept"})
96
+ }
97
+
98
+ func TestCustomLogger_DropsEverythingAtLevelOff(t *testing.T) {
99
+ twin(t, "logging.test.ts | custom logger and level filtering drops everything at level off")
100
+ l := &recordingLogger{}
101
+ _, err := loggedClient(t, l, LogOff).Models().List(ctxBG(), nil)
102
+ noErr(t, err)
103
+ eq(t, len(l.snapshot()), 0)
104
+ }
105
+
106
+ func TestCustomLogger_LogsAOneLineSummaryAtInfoAndNothingAtDebug(t *testing.T) {
107
+ twin(t, "logging.test.ts | custom logger and level filtering logs a one-line summary at info and nothing at debug")
108
+ l := &recordingLogger{}
109
+ _, err := loggedClient(t, l, LogInfo).Models().List(ctxBG(), nil)
110
+ noErr(t, err)
111
+ eq(t, len(l.messages("debug")), 0)
112
+ got := l.messages("info")
113
+ eq(t, len(got), 1)
114
+ if !regexp.MustCompile(`^#1 GET /v1/models <- 200 in \d+ms \(request req_9\)$`).MatchString(got[0]) {
115
+ t.Fatalf("summary %q", got[0])
116
+ }
117
+ }
118
+
119
+ func TestCustomLogger_LogsRequestHeadersRequestBodyAndResponseBodyAtDebug(t *testing.T) {
120
+ twin(t, "logging.test.ts | custom logger and level filtering logs request headers, request body, and response body at debug")
121
+ l := &recordingLogger{}
122
+ _, err := loggedClient(t, l, LogDebug).Models().List(ctxBG(), nil)
123
+ noErr(t, err)
124
+ calls := l.snapshot()
125
+ first := calls[0]
126
+ eq(t, first.Level, "debug")
127
+ eq(t, first.Message, "#1 GET /v1/models -> https://x.test/v1/models")
128
+ detail := first.Args[0].(map[string]any)
129
+ headers := detail["headers"].(map[string]string)
130
+ runtimeHeader := headers["X-TypeSafe-Runtime"]
131
+ if !strings.HasPrefix(runtimeHeader, "go/") {
132
+ t.Fatalf("runtime %q", runtimeHeader)
133
+ }
134
+ eq(t, headers, map[string]string{
135
+ "Authorization": "Bearer ***cdef", "Accept": "application/json",
136
+ "User-Agent": "typesafe-sdk-go/" + Version, "X-TypeSafe-SDK": "typesafe-sdk-go/" + Version, "X-TypeSafe-Runtime": runtimeHeader,
137
+ })
138
+ if detail["body"] != nil {
139
+ t.Fatalf("a GET has no body, got %v", detail["body"])
140
+ }
141
+ last := calls[len(calls)-1]
142
+ eq(t, last.Level, "debug")
143
+ eq(t, last.Message, "#1 GET /v1/models <- body")
144
+ eq(t, last.Args[0], any(map[string]any{"models": []any{}}))
145
+ }
146
+
147
+ func TestCustomLogger_NeverLogsTheRawAPIKey(t *testing.T) {
148
+ twin(t, "logging.test.ts | custom logger and level filtering never logs the raw API key")
149
+ l := &recordingLogger{}
150
+ _, err := loggedClient(t, l, LogDebug).Models().List(ctxBG(), nil)
151
+ noErr(t, err)
152
+ for _, c := range l.snapshot() {
153
+ if strings.Contains(c.Message, "sk_live_0123456789abcdef") || strings.Contains(strings.ReplaceAll(strings.TrimSpace(fmtAny(c.Args)), "\n", " "), "sk_live_0123456789abcdef") {
154
+ t.Fatalf("raw key in log call %+v", c)
155
+ }
156
+ }
157
+ }
158
+
159
+ func TestCustomLogger_NumbersRequestsSoConcurrentCallsCanBeToldApart(t *testing.T) {
160
+ twin(t, "logging.test.ts | custom logger and level filtering numbers requests so concurrent calls can be told apart")
161
+ l := &recordingLogger{}
162
+ c := loggedClient(t, l, LogInfo)
163
+ var wg sync.WaitGroup
164
+ for i := 0; i < 2; i++ {
165
+ wg.Add(1)
166
+ go func() { defer wg.Done(); _, _ = c.Models().List(ctxBG(), nil) }()
167
+ }
168
+ wg.Wait()
169
+ var tags []string
170
+ for _, m := range l.messages("info") {
171
+ tags = append(tags, m[:2])
172
+ }
173
+ if len(tags) != 2 || !((tags[0] == "#1" && tags[1] == "#2") || (tags[0] == "#2" && tags[1] == "#1")) {
174
+ t.Fatalf("tags %v", tags)
175
+ }
176
+ }
177
+
178
+ func TestCustomLogger_LogsErrorResponsesAsASummaryPlusTheBodyAtDebugWithoutAWarn(t *testing.T) {
179
+ twin(t, "logging.test.ts | custom logger and level filtering logs error responses as a summary plus the body at debug, without a warn")
180
+ l := &recordingLogger{}
181
+ c := newClient(t, always(func() *http.Response { return jsonResp(404, map[string]any{"message": "nope"}) }), func(c *Config) { c.Logger, c.LogLevel = l, LogDebug })
182
+ _, err := c.Models().List(ctxBG(), nil)
183
+ if err == nil || !strings.Contains(err.Error(), "404 nope") {
184
+ t.Fatalf("got %v", err)
185
+ }
186
+ info := l.messages("info")
187
+ eq(t, len(info), 1)
188
+ if !regexp.MustCompile(`^#1 GET /v1/models <- 404 in \d+ms$`).MatchString(info[0]) {
189
+ t.Fatalf("summary %q", info[0])
190
+ }
191
+ calls := l.snapshot()
192
+ last := calls[len(calls)-1]
193
+ eq(t, last, logCall{"debug", "#1 GET /v1/models <- error body", []any{map[string]any{"message": "nope"}}})
194
+ eq(t, len(l.messages("warn")), 0)
195
+ eq(t, len(l.messages("error")), 0)
196
+ }
197
+
198
+ func TestCustomLogger_LogsConnectionFailuresWithTheUnderlyingError(t *testing.T) {
199
+ twin(t, "logging.test.ts | custom logger and level filtering logs connection failures with the underlying error")
200
+ l := &recordingLogger{}
201
+ boom := errors.New("fetch failed")
202
+ c := newClient(t, newMock(func(*recordedRequest) (*http.Response, error) { return nil, boom }), func(c *Config) {
203
+ c.Logger, c.LogLevel = l, LogInfo
204
+ c.Retry = RetryOverrides{MaxRetries: Ptr(0)}
205
+ })
206
+ _, err := c.Models().List(ctxBG(), nil)
207
+ if err == nil || !strings.Contains(err.Error(), "fetch failed") {
208
+ t.Fatalf("got %v", err)
209
+ }
210
+ calls := l.snapshot()
211
+ eq(t, len(calls), 1)
212
+ eq(t, calls[0].Level, "info")
213
+ if !regexp.MustCompile(`^#1 GET /v1/models connection error after \d+ms$`).MatchString(calls[0].Message) {
214
+ t.Fatalf("message %q", calls[0].Message)
215
+ }
216
+ if len(calls[0].Args) != 1 || calls[0].Args[0] != error(boom) {
217
+ t.Fatalf("args %v", calls[0].Args)
218
+ }
219
+ }
220
+
221
+ func TestCustomLogger_LogsCallerAborts(t *testing.T) {
222
+ twin(t, "logging.test.ts | custom logger and level filtering logs caller aborts")
223
+ l := &recordingLogger{}
224
+ ctx, cancel := context.WithCancel(context.Background())
225
+ c := newClient(t, newMock(func(*recordedRequest) (*http.Response, error) { cancel(); return nil, context.Canceled }), func(c *Config) { c.Logger, c.LogLevel = l, LogInfo })
226
+ _, err := c.Models().List(ctx, nil)
227
+ mustAs[*APIUserAbortError](t, err)
228
+ info := l.messages("info")
229
+ eq(t, len(info), 1)
230
+ if !regexp.MustCompile(`^#1 GET /v1/models aborted by caller after \d+ms$`).MatchString(info[0]) {
231
+ t.Fatalf("message %q", info[0])
232
+ }
233
+ }
234
+
235
+ func TestRedactHeaders_MasksCredentialsKeepsTheSchemeAndTheLastFourCharacters(t *testing.T) {
236
+ twin(t, "logging.test.ts | redactHeaders masks credentials, keeps the scheme and the last four characters")
237
+ got := RedactHeaders(map[string]string{"Authorization": "Bearer sk_live_0123456789abcdef", "x-api-key": "0123456789abcdef", "Cookie": "session=abc", "Accept": "application/json"})
238
+ eq(t, got, map[string]string{"Authorization": "Bearer ***cdef", "x-api-key": "***cdef", "Cookie": "***", "Accept": "application/json"})
239
+ }
240
+
241
+ func TestRedactHeaders_DoesNotLeakATailFromShortSecrets(t *testing.T) {
242
+ twin(t, "logging.test.ts | redactHeaders does not leak a tail from short secrets")
243
+ eq(t, RedactHeaders(map[string]string{"authorization": "Bearer abc"}), map[string]string{"authorization": "Bearer ***"})
244
+ }
245
+
246
+ func TestRedactHeaders_DoesNotMutateItsInput(t *testing.T) {
247
+ twin(t, "logging.test.ts | redactHeaders does not mutate its input")
248
+ in := map[string]string{"Authorization": "Bearer x"}
249
+ RedactHeaders(in)
250
+ eq(t, in["Authorization"], "Bearer x")
251
+ }
252
+
253
+ func TestParseLogLevel_RejectsUnknownValuesNamingTheSource(t *testing.T) {
254
+ _, err := ParseLogLevel("loud", "somewhere")
255
+ contains(t, err.Error(), `Invalid log level "loud" from somewhere. Expected one of: debug, info, warn, error, off.`)
256
+ l, err := ParseLogLevel("info", "x")
257
+ noErr(t, err)
258
+ eq(t, l, LogInfo)
259
+ }
@@ -0,0 +1,112 @@
1
+ package typesafe
2
+
3
+ import (
4
+ "bytes"
5
+ "encoding/json"
6
+ "testing"
7
+ )
8
+
9
+ // encoding/json passes the output of a Marshaler (and a json.RawMessage) through compact, and Go 1.26
10
+ // copies U+2028 and U+2029 through unescaped there when HTML escaping is off, while Go 1.27 always escapes
11
+ // them (as it always has for a plain string). A body must not depend on the toolchain that built the client
12
+ // (its Content-Length would not), so marshalPlain escapes them everywhere; a parser decodes both forms alike.
13
+ func TestMarshalPlainEscapesLineSeparatorsOnEveryGoVersion(t *testing.T) {
14
+ raw := json.RawMessage("{\"k\":\"a\u2028b\u2029c\"}")
15
+ cases := map[string]any{
16
+ "string": "a\u2028b\u2029c",
17
+ "raw message": raw,
18
+ "entry": Value(raw),
19
+ "nested": map[string]any{"state": Value(raw), "text": Text("a\u2028b")},
20
+ }
21
+ for name, v := range cases {
22
+ t.Run(name, func(t *testing.T) {
23
+ got, err := marshalPlain(v)
24
+ if err != nil {
25
+ t.Fatal(err)
26
+ }
27
+ if bytes.Contains(got, []byte("\u2028")) || bytes.Contains(got, []byte("\u2029")) {
28
+ t.Fatalf("raw U+2028/U+2029 left in %q", got)
29
+ }
30
+ var back any
31
+ if err := json.Unmarshal(got, &back); err != nil {
32
+ t.Fatal(err)
33
+ }
34
+ })
35
+ }
36
+ }
37
+
38
+ // On Go 1.27 encoding/json never hands marshalPlain a raw separator, so the test above cannot see the escaping
39
+ // step there; this one feeds it the raw bytes Go 1.26 produces, so the step is checked on every toolchain.
40
+ func TestEscapeLineSeparatorsReplacesTheRawBytesGo126Leaves(t *testing.T) {
41
+ got := string(escapeLineSeparators([]byte("{\"k\":\"a\u2028b\u2029c\\\\u2028\"}")))
42
+ if want := `{"k":"a\u2028b\u2029c\\u2028"}`; got != want {
43
+ t.Fatalf("got %s, want %s", got, want)
44
+ }
45
+ }
46
+
47
+ func TestMarshalPlainKeepsHTMLAndAnEscapedBackslashIntact(t *testing.T) {
48
+ // A backslash followed by "u2028" is the text \u2028, not a separator, and must stay as written.
49
+ got, err := marshalPlain(map[string]any{"a": "<b>&\\u2028"})
50
+ if err != nil {
51
+ t.Fatal(err)
52
+ }
53
+ if want := `{"a":"<b>&\\u2028"}`; string(got) != want {
54
+ t.Fatalf("got %s, want %s", got, want)
55
+ }
56
+ }
57
+
58
+ // Most bodies hold neither separator. The replacement then must not copy the body (bytes.ReplaceAll returns a
59
+ // fresh copy even when nothing matches, and every Entry in a request goes through it).
60
+ func TestEscapeLineSeparatorsDoesNotCopyACleanBody(t *testing.T) {
61
+ clean := []byte(`{"state":"plain text","n":1}`)
62
+ if allocs := testing.AllocsPerRun(100, func() { _ = escapeLineSeparators(clean) }); allocs != 0 {
63
+ t.Fatalf("escapeLineSeparators allocated %.0f times for a body without U+2028 or U+2029", allocs)
64
+ }
65
+ got := escapeLineSeparators([]byte("{\"a\":\"x\u2028y\u2029z\"}"))
66
+ if want := `{"a":"x\u2028y\u2029z"}`; string(got) != want {
67
+ t.Fatalf("got %q, want %q", got, want)
68
+ }
69
+ }
70
+
71
+ // A key is written the way marshalPlain writes it, but a plain one (the SDK's own field names, and a question
72
+ // name such as "destructive") does not need an encoder: it is quoted directly.
73
+ func TestObjectKeyMatchesMarshalPlainAndSkipsTheEncoderForPlainNames(t *testing.T) {
74
+ for _, name := range []string{"", "state", "beyond_scope", "with space", `quo"te`, `back\slash`, "tab\there", "ünï", "<tag>&", "a\u2028b", "\x7f", "emoji 🐖"} {
75
+ o := newObject()
76
+ o.key(name)
77
+ want, _ := marshalPlain(name)
78
+ if got := o.buf.String(); got != "{"+string(want)+":" {
79
+ t.Errorf("key(%q) wrote %q, want %q", name, got, "{"+string(want)+":")
80
+ }
81
+ }
82
+ if allocs := testing.AllocsPerRun(100, func() { o := newObject(); o.key("destructive") }); allocs > 3 {
83
+ t.Fatalf("a plain key allocated %.0f times; the object and its buffer are the only allocations it needs", allocs)
84
+ }
85
+ }
86
+
87
+ // Below debug level nothing reads the request headers or the response body that debug logging prints, so they
88
+ // are not built: logBody parsed the whole response as JSON for a log line that the level filter then dropped.
89
+ func TestDebugLogArgumentsAreNotBuiltBelowDebugLevel(t *testing.T) {
90
+ body := []byte(`{"model":"m","answers":{"a":{"type":"noul","noul":0.5}},"usage":{}}`)
91
+ for _, level := range []LogLevel{LogOff, LogError, LogWarn, LogInfo} {
92
+ c, err := NewClient(Config{APIKey: "k", BaseURL: "http://127.0.0.1:1", Getenv: noEnv, LogLevel: level})
93
+ noErr(t, err)
94
+ raw := &RawResponse{Status: 200, Body: body}
95
+ if allocs := testing.AllocsPerRun(50, func() { c.logBody(raw) }); allocs != 0 {
96
+ t.Errorf("level %s: logBody allocated %.0f times; the body must not be parsed for a debug line that is dropped", level, allocs)
97
+ }
98
+ }
99
+ c, err := NewClient(Config{APIKey: "k", BaseURL: "http://127.0.0.1:1", Getenv: noEnv, LogLevel: LogDebug, Logger: &captureLogger{}})
100
+ noErr(t, err)
101
+ c.logBody(&RawResponse{Status: 200, Body: body, tag: "#1 POST /x"})
102
+ if cl := c.logger.(levelLogger).sink.(*captureLogger); len(cl.debug) != 1 {
103
+ t.Fatalf("debug level must still log the parsed body, got %v", cl.debug)
104
+ }
105
+ }
106
+
107
+ type captureLogger struct{ debug []string }
108
+
109
+ func (l *captureLogger) Debug(m string, _ ...any) { l.debug = append(l.debug, m) }
110
+ func (l *captureLogger) Info(string, ...any) {}
111
+ func (l *captureLogger) Warn(string, ...any) {}
112
+ func (l *captureLogger) Error(string, ...any) {}
@@ -0,0 +1,39 @@
1
+ package typesafe
2
+
3
+ import (
4
+ "context"
5
+ "sync"
6
+ "testing"
7
+ "time"
8
+ )
9
+
10
+ // Added by the mutation check (port/mutations.json: batch-default-concurrency-one): with no
11
+ // Concurrency set, DefaultBatchConcurrency requests run at once.
12
+ func TestEvaluateBatch_DefaultConcurrencyRunsSeveralRequestsAtOnce(t *testing.T) {
13
+ var mu sync.Mutex
14
+ inflight, peak := 0, 0
15
+ release := make(chan struct{})
16
+ var once sync.Once
17
+ ev := evalFunc(func(ctx context.Context, req SystemOneRequest, _ *RequestOptions) (*SystemOneResult, error) {
18
+ mu.Lock()
19
+ inflight++
20
+ if inflight > peak {
21
+ peak = inflight
22
+ }
23
+ if inflight == DefaultBatchConcurrency {
24
+ once.Do(func() { close(release) })
25
+ }
26
+ mu.Unlock()
27
+ select {
28
+ case <-release:
29
+ case <-time.After(2 * time.Second):
30
+ }
31
+ mu.Lock()
32
+ inflight--
33
+ mu.Unlock()
34
+ return &SystemOneResult{}, nil
35
+ })
36
+ out := EvaluateBatch(context.Background(), ev, batchItems(2*DefaultBatchConcurrency), BatchOptions{})
37
+ eq(t, len(out), 2*DefaultBatchConcurrency)
38
+ eq(t, peak, DefaultBatchConcurrency)
39
+ }