@pi-in-go/pigpen-pi-typesafe 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (189) hide show
  1. package/CREDITS.md +14 -0
  2. package/LICENSE +22 -0
  3. package/README.md +45 -0
  4. package/extensions/pi-typesafe/branches_test.go +185 -0
  5. package/extensions/pi-typesafe/command.go +319 -0
  6. package/extensions/pi-typesafe/export_test.go +9 -0
  7. package/extensions/pi-typesafe/extension.go +188 -0
  8. package/extensions/pi-typesafe/extension_test.go +321 -0
  9. package/extensions/pi-typesafe/fakehost_test.go +548 -0
  10. package/extensions/pi-typesafe/format.go +191 -0
  11. package/extensions/pi-typesafe/format_test.go +75 -0
  12. package/extensions/pi-typesafe/go.mod +10 -0
  13. package/extensions/pi-typesafe/go.sum +2 -0
  14. package/extensions/pi-typesafe/go.work +11 -0
  15. package/extensions/pi-typesafe/harness_test.go +200 -0
  16. package/extensions/pi-typesafe/ownmodel_test.go +100 -0
  17. package/extensions/pi-typesafe/review_test.go +134 -0
  18. package/extensions/pi-typesafe/tool.go +193 -0
  19. package/extensions/pi-typesafe/twin_test.go +28 -0
  20. package/libs/pi-typesafe-api/CREDITS.md +14 -0
  21. package/libs/pi-typesafe-api/LICENSE +22 -0
  22. package/libs/pi-typesafe-api/README.md +30 -0
  23. package/libs/pi-typesafe-api/ask.go +62 -0
  24. package/libs/pi-typesafe-api/ask_test.go +76 -0
  25. package/libs/pi-typesafe-api/auth.go +249 -0
  26. package/libs/pi-typesafe-api/auth_test.go +131 -0
  27. package/libs/pi-typesafe-api/backends.go +336 -0
  28. package/libs/pi-typesafe-api/backends_test.go +404 -0
  29. package/libs/pi-typesafe-api/batch.go +202 -0
  30. package/libs/pi-typesafe-api/batch_test.go +202 -0
  31. package/libs/pi-typesafe-api/battery_test.go +41 -0
  32. package/libs/pi-typesafe-api/calibrate.go +354 -0
  33. package/libs/pi-typesafe-api/calibrate_test.go +186 -0
  34. package/libs/pi-typesafe-api/client.go +615 -0
  35. package/libs/pi-typesafe-api/client_test.go +490 -0
  36. package/libs/pi-typesafe-api/credentials.go +252 -0
  37. package/libs/pi-typesafe-api/credentials_test.go +216 -0
  38. package/libs/pi-typesafe-api/doc.go +14 -0
  39. package/libs/pi-typesafe-api/errors.go +143 -0
  40. package/libs/pi-typesafe-api/evaluation.go +86 -0
  41. package/libs/pi-typesafe-api/evaluation_schema.json +264 -0
  42. package/libs/pi-typesafe-api/gaps_test.go +77 -0
  43. package/libs/pi-typesafe-api/go.mod +9 -0
  44. package/libs/pi-typesafe-api/go.sum +2 -0
  45. package/libs/pi-typesafe-api/helpers_test.go +169 -0
  46. package/libs/pi-typesafe-api/hostmodel/hostmodel.go +87 -0
  47. package/libs/pi-typesafe-api/json.go +299 -0
  48. package/libs/pi-typesafe-api/json_test.go +92 -0
  49. package/libs/pi-typesafe-api/ownmodel_test.go +79 -0
  50. package/libs/pi-typesafe-api/package.json +40 -0
  51. package/libs/pi-typesafe-api/provenance.json +18 -0
  52. package/libs/pi-typesafe-api/review_test.go +23 -0
  53. package/libs/pi-typesafe-api/schema.go +473 -0
  54. package/libs/pi-typesafe-api/schema_test.go +262 -0
  55. package/libs/pi-typesafe-api/testdata/tools/typebox-messages.mts +5 -0
  56. package/libs/pi-typesafe-api/testdata/typebox-messages.json +285 -0
  57. package/libs/pi-typesafe-api/twin_test.go +28 -0
  58. package/libs/pi-typesafe-api/ui/fakehost_test.go +548 -0
  59. package/libs/pi-typesafe-api/ui/keyprompt.go +115 -0
  60. package/libs/pi-typesafe-api/ui/login.go +106 -0
  61. package/libs/pi-typesafe-api/ui/twin_test.go +28 -0
  62. package/libs/pi-typesafe-api/ui/ui_test.go +285 -0
  63. package/libs/pi-typesafe-api/usage.go +366 -0
  64. package/libs/pi-typesafe-api/usage_test.go +139 -0
  65. package/libs/typesafe/CONTRACT.md +125 -0
  66. package/libs/typesafe/CREDITS.md +37 -0
  67. package/libs/typesafe/LICENSE +23 -0
  68. package/libs/typesafe/README.md +19 -0
  69. package/libs/typesafe/go.mod +3 -0
  70. package/libs/typesafe/libraries/ownmodel/backend_test.go +496 -0
  71. package/libs/typesafe/libraries/ownmodel/canon.go +190 -0
  72. package/libs/typesafe/libraries/ownmodel/convert.go +199 -0
  73. package/libs/typesafe/libraries/ownmodel/doc.go +15 -0
  74. package/libs/typesafe/libraries/ownmodel/equivalence_test.go +199 -0
  75. package/libs/typesafe/libraries/ownmodel/helpers_test.go +155 -0
  76. package/libs/typesafe/libraries/ownmodel/mutation_test.go +31 -0
  77. package/libs/typesafe/libraries/ownmodel/ownmodel.go +225 -0
  78. package/libs/typesafe/libraries/ownmodel/plan.go +442 -0
  79. package/libs/typesafe/libraries/ownmodel/run.go +288 -0
  80. package/libs/typesafe/libraries/ownmodel/schema_test.go +254 -0
  81. package/libs/typesafe/libraries/ownmodel/twins_test.go +169 -0
  82. package/libs/typesafe/libraries/ownmodel/utils_test.go +125 -0
  83. package/libs/typesafe/libraries/pigmodel/pigmodel.go +264 -0
  84. package/libs/typesafe/libraries/pigmodel/pigmodel_test.go +410 -0
  85. package/libs/typesafe/libraries/typesafe/answers.go +268 -0
  86. package/libs/typesafe/libraries/typesafe/api_response_test.go +113 -0
  87. package/libs/typesafe/libraries/typesafe/batch.go +80 -0
  88. package/libs/typesafe/libraries/typesafe/batch_test.go +133 -0
  89. package/libs/typesafe/libraries/typesafe/bench_test.go +71 -0
  90. package/libs/typesafe/libraries/typesafe/client.go +561 -0
  91. package/libs/typesafe/libraries/typesafe/client_test.go +495 -0
  92. package/libs/typesafe/libraries/typesafe/crosscheck_test.go +464 -0
  93. package/libs/typesafe/libraries/typesafe/crosscheck_workflowevals_test.go +219 -0
  94. package/libs/typesafe/libraries/typesafe/doc.go +27 -0
  95. package/libs/typesafe/libraries/typesafe/entry.go +142 -0
  96. package/libs/typesafe/libraries/typesafe/env.go +11 -0
  97. package/libs/typesafe/libraries/typesafe/errors.go +310 -0
  98. package/libs/typesafe/libraries/typesafe/errors_test.go +175 -0
  99. package/libs/typesafe/libraries/typesafe/helpers_test.go +294 -0
  100. package/libs/typesafe/libraries/typesafe/live_test.go +96 -0
  101. package/libs/typesafe/libraries/typesafe/logging.go +160 -0
  102. package/libs/typesafe/libraries/typesafe/logging_test.go +259 -0
  103. package/libs/typesafe/libraries/typesafe/marshal_test.go +112 -0
  104. package/libs/typesafe/libraries/typesafe/mutation_test.go +39 -0
  105. package/libs/typesafe/libraries/typesafe/questions.go +490 -0
  106. package/libs/typesafe/libraries/typesafe/questions_test.go +166 -0
  107. package/libs/typesafe/libraries/typesafe/regressions_test.go +159 -0
  108. package/libs/typesafe/libraries/typesafe/reliability_test.go +649 -0
  109. package/libs/typesafe/libraries/typesafe/retry.go +350 -0
  110. package/libs/typesafe/libraries/typesafe/retry_test.go +297 -0
  111. package/libs/typesafe/libraries/typesafe/runtime_test.go +26 -0
  112. package/libs/typesafe/libraries/typesafe/transport_test.go +163 -0
  113. package/libs/typesafe/libraries/typesafe/twins_test.go +127 -0
  114. package/libs/typesafe/libraries/typesafe/types_test.go +165 -0
  115. package/libs/typesafe/libraries/typesafe/version.go +10 -0
  116. package/libs/typesafe/package.json +37 -0
  117. package/libs/typesafe/provenance.json +49 -0
  118. package/package.json +42 -0
  119. package/port/PORT.md +98 -0
  120. package/port/accepted-gaps.json +3 -0
  121. package/port/golden/enable-confirm.jsonl +11 -0
  122. package/port/golden/enable-decline.jsonl +20 -0
  123. package/port/golden/enable-missing-key.jsonl +4 -0
  124. package/port/golden/login-shadow.jsonl +4 -0
  125. package/port/golden/logout-env-key.jsonl +6 -0
  126. package/port/golden/playground-cancel.jsonl +4 -0
  127. package/port/golden/playground-invalid-json.jsonl +5 -0
  128. package/port/golden/playground-invalid-questions.jsonl +5 -0
  129. package/port/golden/status-env-key.jsonl +6 -0
  130. package/port/golden/status-no-key.jsonl +6 -0
  131. package/port/golden/tool-disabled.jsonl +18 -0
  132. package/port/golden/trailing-words.jsonl +10 -0
  133. package/port/library-mutations.py +44 -0
  134. package/port/mutations.json +302 -0
  135. package/port/oracle/.env.example +4 -0
  136. package/port/oracle/CHANGELOG.md +91 -0
  137. package/port/oracle/CONTRIBUTING.md +35 -0
  138. package/port/oracle/LICENSE +21 -0
  139. package/port/oracle/README.md +159 -0
  140. package/port/oracle/docs/api.md +143 -0
  141. package/port/oracle/docs/ci-cd.md +97 -0
  142. package/port/oracle/examples/decision-extension.ts +41 -0
  143. package/port/oracle/extensions/index.js +2 -0
  144. package/port/oracle/package.json +89 -0
  145. package/port/oracle/scripts/dev-pi.mjs +23 -0
  146. package/port/oracle/scripts/live-smoke.mjs +35 -0
  147. package/port/oracle/src/ask.ts +42 -0
  148. package/port/oracle/src/auth.ts +171 -0
  149. package/port/oracle/src/backends.ts +196 -0
  150. package/port/oracle/src/batch.ts +170 -0
  151. package/port/oracle/src/calibrate.ts +237 -0
  152. package/port/oracle/src/client.ts +310 -0
  153. package/port/oracle/src/credentials.ts +136 -0
  154. package/port/oracle/src/errors.ts +53 -0
  155. package/port/oracle/src/extension.ts +204 -0
  156. package/port/oracle/src/index.ts +31 -0
  157. package/port/oracle/src/key-prompt.ts +51 -0
  158. package/port/oracle/src/login.ts +60 -0
  159. package/port/oracle/src/schema.ts +158 -0
  160. package/port/oracle/src/ui.ts +4 -0
  161. package/port/oracle/src/usage.ts +258 -0
  162. package/port/oracle/tests/ask.test.ts +63 -0
  163. package/port/oracle/tests/auth.test.ts +141 -0
  164. package/port/oracle/tests/backends.test.ts +380 -0
  165. package/port/oracle/tests/batch.test.ts +156 -0
  166. package/port/oracle/tests/calibrate.test.ts +144 -0
  167. package/port/oracle/tests/client.test.ts +499 -0
  168. package/port/oracle/tests/credentials.test.ts +144 -0
  169. package/port/oracle/tests/extension.test.ts +276 -0
  170. package/port/oracle/tests/key-prompt.test.ts +47 -0
  171. package/port/oracle/tests/login.test.ts +101 -0
  172. package/port/oracle/tests/schema.test.ts +85 -0
  173. package/port/oracle/tests/usage.test.ts +106 -0
  174. package/port/oracle/tsconfig.build.json +10 -0
  175. package/port/oracle/tsconfig.json +14 -0
  176. package/port/scenarios/enable-confirm.json +5 -0
  177. package/port/scenarios/enable-decline.json +3 -0
  178. package/port/scenarios/enable-missing-key.json +2 -0
  179. package/port/scenarios/login-shadow.json +2 -0
  180. package/port/scenarios/logout-env-key.json +3 -0
  181. package/port/scenarios/playground-cancel.json +2 -0
  182. package/port/scenarios/playground-invalid-json.json +2 -0
  183. package/port/scenarios/playground-invalid-questions.json +2 -0
  184. package/port/scenarios/status-env-key.json +3 -0
  185. package/port/scenarios/status-no-key.json +3 -0
  186. package/port/scenarios/tool-disabled.json +2 -0
  187. package/port/scenarios/trailing-words.json +5 -0
  188. package/port/upstream-tests.json +160 -0
  189. package/provenance.json +18 -0
@@ -0,0 +1,249 @@
1
+ package pitypesafe
2
+
3
+ import (
4
+ "encoding/json"
5
+ "os"
6
+ "path/filepath"
7
+ "time"
8
+ )
9
+
10
+ const authVersion = 1
11
+
12
+ // AuthFailure is the last request that degraded TypeSafe, with no upstream body, header, key, or submitted state.
13
+ type AuthFailure struct {
14
+ Code ErrorCode `json:"code"`
15
+ Status int `json:"status,omitempty"`
16
+ Message string `json:"message"`
17
+ At string `json:"at"`
18
+ }
19
+
20
+ // AuthState is the whole answer to "is Jev actually available right now": which key is in effect, whether it
21
+ // has been accepted, and the last failure that degraded it. Consumers must consult it instead of treating their
22
+ // own consent flag as proof that judgments will happen.
23
+ type AuthState struct {
24
+ // Backend is the value the caller passed (nil means the default).
25
+ Backend any
26
+ Kind KeyKind
27
+ Source KeySource
28
+ // Path is where the key would be read from.
29
+ Path string
30
+ // Reason says why a stored key cannot be used.
31
+ Reason string
32
+ // KeyName is a short label for the key source: TYPESAFE_API_KEY, /typesafe login, no key, unusable key.
33
+ KeyName string
34
+ // Verified is true when the key in effect was accepted by the backend.
35
+ Verified bool
36
+ VerifiedAt string
37
+ // LastFailure is cleared by the next successful request.
38
+ LastFailure *AuthFailure
39
+ // Usable means a key is present and the last authentication outcome was not a rejection. False means judgments are skipped.
40
+ Usable bool
41
+ }
42
+
43
+ // AuthOptions selects the state file and backend.
44
+ type AuthOptions struct {
45
+ Path string
46
+ Backend any
47
+ }
48
+
49
+ // AuthStatePath is the auth-state file: one small, owner-only record that outlives the process that wrote it.
50
+ func AuthStatePath() string { return filepath.Join(TypeSafeDir(), "auth-state.json") }
51
+
52
+ type storedAuth struct {
53
+ Version int `json:"version"`
54
+ VerifiedAt string `json:"verifiedAt,omitempty"`
55
+ LastFailure *AuthFailure `json:"lastFailure,omitempty"`
56
+ }
57
+
58
+ var errorCodes = map[ErrorCode]bool{CodeConfiguration: true, CodeValidation: true, CodeBudget: true, CodeAborted: true, CodeTimeout: true, CodeHTTP: true, CodeConnection: true, CodeResponse: true}
59
+
60
+ func readAuthState(path string) storedAuth {
61
+ data, err := os.ReadFile(path)
62
+ if err != nil {
63
+ return storedAuth{}
64
+ }
65
+ var raw struct {
66
+ VerifiedAt any `json:"verifiedAt"`
67
+ LastFailure *struct {
68
+ Code any `json:"code"`
69
+ Status any `json:"status"`
70
+ Message any `json:"message"`
71
+ At any `json:"at"`
72
+ } `json:"lastFailure"`
73
+ }
74
+ if json.Unmarshal(data, &raw) != nil {
75
+ return storedAuth{}
76
+ }
77
+ var out storedAuth
78
+ if v, ok := raw.VerifiedAt.(string); ok && utf16Len(v) <= 40 {
79
+ out.VerifiedAt = v
80
+ }
81
+ if f := raw.LastFailure; f != nil {
82
+ code, _ := f.Code.(string)
83
+ message, _ := f.Message.(string)
84
+ at, _ := f.At.(string)
85
+ if errorCodes[ErrorCode(code)] && message != "" && at != "" && utf16Len(at) <= 40 {
86
+ failure := &AuthFailure{Code: ErrorCode(code), Message: truncateUTF16(message, 300), At: at}
87
+ if s, ok := f.Status.(float64); ok && s == float64(int(s)) {
88
+ failure.Status = int(s)
89
+ }
90
+ out.LastFailure = failure
91
+ }
92
+ }
93
+ return out
94
+ }
95
+
96
+ func truncateUTF16(s string, n int) string {
97
+ if utf16Len(s) <= n {
98
+ return s
99
+ }
100
+ count := 0
101
+ for i, r := range s {
102
+ w := 1
103
+ if r >= 0x10000 {
104
+ w = 2
105
+ }
106
+ if count+w > n {
107
+ return s[:i]
108
+ }
109
+ count += w
110
+ }
111
+ return s
112
+ }
113
+
114
+ // writeAuthState is owner-only, atomic, and best-effort: an unwritable auth record never changes how a request behaves.
115
+ func writeAuthState(path string, state storedAuth) {
116
+ state.Version = authVersion
117
+ body, err := json.MarshalIndent(state, "", " ")
118
+ if err != nil {
119
+ return
120
+ }
121
+ _ = writeOwnerOnly(path, append(body, '\n'))
122
+ }
123
+
124
+ func rejected(f *AuthFailure) bool {
125
+ return f != nil && f.Code == CodeHTTP && (f.Status == 401 || f.Status == 403)
126
+ }
127
+
128
+ // GetAuthState adds the key situation, the last outcome, and the clock up for a valid backend. An invalid
129
+ // backend returns the same configuration error as ResolveBackend, so validate a user-supplied endpoint with
130
+ // ResolveBackend first.
131
+ func GetAuthState(opts AuthOptions) (AuthState, error) {
132
+ path := opts.Path
133
+ if path == "" {
134
+ path = AuthStatePath()
135
+ }
136
+ backend := opts.Backend
137
+ if backend == nil {
138
+ backend = DefaultBackend
139
+ }
140
+ situation, err := KeySituationFor(backend)
141
+ if err != nil {
142
+ return AuthState{}, err
143
+ }
144
+ stored := readAuthState(path)
145
+ var source KeySource
146
+ switch situation.Kind {
147
+ case KeyEnvironment:
148
+ source = SourceEnvironment
149
+ case KeyStored:
150
+ source = SourceStored
151
+ }
152
+ isRejected := rejected(stored.LastFailure)
153
+ present := source != "" || situation.Kind == KeyNotRequired
154
+ state := AuthState{
155
+ Backend: backend,
156
+ Kind: situation.Kind,
157
+ Source: source,
158
+ Path: CredentialsPath(),
159
+ KeyName: KeySourceLabel(situation),
160
+ Verified: stored.VerifiedAt != "" && !isRejected,
161
+ VerifiedAt: stored.VerifiedAt,
162
+ LastFailure: stored.LastFailure,
163
+ Usable: present && !isRejected,
164
+ }
165
+ if situation.Kind == KeyUnusable {
166
+ state.Path = situation.Path
167
+ state.Reason = situation.Reason
168
+ }
169
+ return state, nil
170
+ }
171
+
172
+ // RecordAuthVerified records that the key was accepted: login verification, or any successful request. It clears the last failure.
173
+ func RecordAuthVerified(at time.Time) {
174
+ writeAuthState(AuthStatePath(), storedAuth{VerifiedAt: isoTime(at)})
175
+ }
176
+
177
+ // RecordAuthFailure records the failure that degraded TypeSafe. The verification timestamp is kept so a recovered key stays known.
178
+ func RecordAuthFailure(err *IntegrationError, at time.Time) {
179
+ current := readAuthState(AuthStatePath())
180
+ writeAuthState(AuthStatePath(), storedAuth{
181
+ VerifiedAt: current.VerifiedAt,
182
+ LastFailure: &AuthFailure{Code: err.Code, Status: err.Status, Message: err.Message, At: isoTime(at)},
183
+ })
184
+ }
185
+
186
+ // ClearAuthState forgets verification and degradation: used when the key itself changes (login or logout).
187
+ func ClearAuthState() { _ = os.Remove(AuthStatePath()) }
188
+
189
+ func isoTime(t time.Time) string { return t.UTC().Format("2006-01-02T15:04:05.000Z") }
190
+
191
+ // AuthLevel is the severity of an AuthReport.
192
+ type AuthLevel string
193
+
194
+ // The levels: error when judgments are skipped or were rejected, warning when the key is unverified, else ok.
195
+ const (
196
+ LevelOK AuthLevel = "ok"
197
+ LevelWarning AuthLevel = "warning"
198
+ LevelError AuthLevel = "error"
199
+ )
200
+
201
+ // AuthReport is one line plus a level, so a status command, a headless log, or a consumer's own status line
202
+ // can call out a degraded state instead of reporting "enabled".
203
+ type AuthReport struct {
204
+ Level AuthLevel
205
+ Text string
206
+ }
207
+
208
+ // DescribeAuth turns a state into a level and one safe line.
209
+ func DescribeAuth(state AuthState) (AuthReport, error) {
210
+ config, err := ResolveBackend(state.Backend)
211
+ if err != nil {
212
+ return AuthReport{}, err
213
+ }
214
+ label := config.Label + " key"
215
+ since := ""
216
+ if f := state.LastFailure; f != nil {
217
+ since = " Last failure: " + f.Message
218
+ if f.At != "" {
219
+ since += " (" + f.At + ")"
220
+ }
221
+ }
222
+ switch state.Kind {
223
+ case KeyNotRequired:
224
+ return AuthReport{LevelOK, config.Label + ": no key needed; judgments use the model PiG is configured with." + since}, nil
225
+ case KeyMissing:
226
+ how := config.KeyEnv + " is set in the environment"
227
+ if UsesTypeSafeKey(config.BackendConfig) {
228
+ how = "a key is configured (/typesafe login or " + typesafeKeyEnv + ")"
229
+ }
230
+ return AuthReport{LevelError, label + ": missing — every Jev judgment is skipped until " + how + "." + since}, nil
231
+ case KeyUnusable:
232
+ reason := state.Reason
233
+ if reason == "" {
234
+ reason = "unknown reason"
235
+ }
236
+ return AuthReport{LevelError, label + ": unusable (" + reason + ") — judgments are skipped until the key is fixed." + since}, nil
237
+ }
238
+ if rejected(state.LastFailure) {
239
+ return AuthReport{LevelError, label + ": " + state.KeyName + " was rejected." + since}, nil
240
+ }
241
+ if !state.Verified {
242
+ return AuthReport{LevelWarning, label + ": " + state.KeyName + " (not verified yet — the first request proves it)." + since}, nil
243
+ }
244
+ verified := "verified"
245
+ if state.VerifiedAt != "" {
246
+ verified += " " + state.VerifiedAt
247
+ }
248
+ return AuthReport{LevelOK, label + ": " + state.KeyName + " (" + verified + ")." + since}, nil
249
+ }
@@ -0,0 +1,131 @@
1
+ package pitypesafe
2
+
3
+ import (
4
+ "os"
5
+ "runtime"
6
+ "strings"
7
+ "testing"
8
+ "time"
9
+ )
10
+
11
+ func mustAuth(t *testing.T, backend any) AuthState {
12
+ t.Helper()
13
+ s, err := GetAuthState(AuthOptions{Backend: backend})
14
+ if err != nil {
15
+ t.Fatal(err)
16
+ }
17
+ return s
18
+ }
19
+
20
+ func mustDescribe(t *testing.T, s AuthState) AuthReport {
21
+ t.Helper()
22
+ r, err := DescribeAuth(s)
23
+ if err != nil {
24
+ t.Fatal(err)
25
+ }
26
+ return r
27
+ }
28
+
29
+ func TestAuth(t *testing.T) {
30
+ tw(t, "auth", "no key at all is an error the consumer cannot mistake for a working setup", func(t *testing.T) {
31
+ isolate(t)
32
+ s := mustAuth(t, nil)
33
+ if s.Kind != KeyMissing || s.KeyName != "no key" || s.Usable || s.Verified {
34
+ t.Fatalf("state = %+v", s)
35
+ }
36
+ r := mustDescribe(t, s)
37
+ if r.Level != LevelError || !strings.Contains(r.Text, "every Jev judgment is skipped") {
38
+ t.Fatalf("report = %+v", r)
39
+ }
40
+ })
41
+ tw(t, "auth", "another backend reports its own key and never the TypeSafe login hint", func(t *testing.T) {
42
+ isolate(t)
43
+ t.Setenv("TYPESAFE_API_KEY", "env-key-0123456789abcdef")
44
+ if mustAuth(t, nil).Backend != "typesafe" {
45
+ t.Error("the default backend is typesafe")
46
+ }
47
+ missing := mustAuth(t, "openrouter")
48
+ if missing.Backend != "openrouter" || missing.Kind != KeyMissing || missing.Usable {
49
+ t.Fatalf("missing = %+v", missing)
50
+ }
51
+ r := mustDescribe(t, missing)
52
+ if r.Level != LevelError || !strings.HasPrefix(r.Text, "OpenRouter key: missing") || !strings.Contains(r.Text, "OPENROUTER_API_KEY is set") || strings.Contains(r.Text, "/typesafe login") {
53
+ t.Fatalf("report = %+v", r)
54
+ }
55
+ t.Setenv("OPENROUTER_API_KEY", "sk-or-0123456789abcdef")
56
+ present := mustAuth(t, "openrouter")
57
+ if present.Kind != KeyEnvironment || present.KeyName != "OPENROUTER_API_KEY" || !present.Usable || !strings.HasPrefix(mustDescribe(t, present).Text, "OpenRouter key: OPENROUTER_API_KEY") {
58
+ t.Fatalf("present = %+v", present)
59
+ }
60
+ })
61
+ tw(t, "auth", "an environment key is usable but unverified until something proves it", func(t *testing.T) {
62
+ isolate(t)
63
+ t.Setenv("TYPESAFE_API_KEY", "env-key-0123456789abcdef")
64
+ before := mustDescribe(t, mustAuth(t, nil))
65
+ if before.Level != LevelWarning || !strings.Contains(before.Text, "TYPESAFE_API_KEY") || !strings.Contains(before.Text, "not verified yet") {
66
+ t.Fatalf("before = %+v", before)
67
+ }
68
+ RecordAuthVerified(time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC))
69
+ s := mustAuth(t, nil)
70
+ if !s.Usable || !s.Verified || s.VerifiedAt != "2026-01-01T00:00:00.000Z" || mustDescribe(t, s).Level != LevelOK {
71
+ t.Fatalf("state = %+v", s)
72
+ }
73
+ if info, _ := os.Stat(AuthStatePath()); runtime.GOOS != "windows" && info.Mode().Perm() != 0o600 {
74
+ t.Errorf("mode = %v", info.Mode().Perm())
75
+ }
76
+ })
77
+ tw(t, "auth", "a rejected key degrades loudly and a later success clears it", func(t *testing.T) {
78
+ isolate(t)
79
+ _, _ = StoreAPIKey("stored-key-0123456789abcdef")
80
+ RecordAuthVerified(time.Date(2026, 1, 2, 0, 0, 0, 0, time.UTC))
81
+ msg := "TypeSafe returned HTTP 401. Check TYPESAFE_API_KEY. No automatic retry was made."
82
+ RecordAuthFailure(&IntegrationError{Code: CodeHTTP, Message: msg, Status: 401}, time.Date(2026, 1, 2, 1, 0, 0, 0, time.UTC))
83
+ d := mustAuth(t, nil)
84
+ want := AuthFailure{Code: CodeHTTP, Status: 401, Message: msg, At: "2026-01-02T01:00:00.000Z"}
85
+ if d.Kind != KeyStored || d.Usable || d.Verified || d.LastFailure == nil || *d.LastFailure != want {
86
+ t.Fatalf("degraded = %+v", d)
87
+ }
88
+ if r := mustDescribe(t, d); r.Level != LevelError || !strings.Contains(r.Text, "was rejected") {
89
+ t.Fatalf("report = %+v", r)
90
+ }
91
+ // A successful request proves the key again and drops the failure.
92
+ RecordAuthVerified(time.Date(2026, 1, 2, 2, 0, 0, 0, time.UTC))
93
+ if r := mustAuth(t, nil); !r.Usable || !r.Verified || r.LastFailure != nil {
94
+ t.Fatalf("recovered = %+v", r)
95
+ }
96
+ })
97
+ tw(t, "auth", "a non-authentication failure degrades the signal without pretending the key is gone", func(t *testing.T) {
98
+ isolate(t)
99
+ _, _ = StoreAPIKey("stored-key-0123456789abcdef")
100
+ RecordAuthVerified(time.Date(2026, 1, 3, 0, 0, 0, 0, time.UTC))
101
+ RecordAuthFailure(&IntegrationError{Code: CodeTimeout, Message: "TypeSafe request timed out; it was not retried and may still be billed."}, time.Date(2026, 1, 3, 0, 5, 0, 0, time.UTC))
102
+ s := mustAuth(t, nil)
103
+ r := mustDescribe(t, s)
104
+ if !s.Usable || !s.Verified || r.Level != LevelOK || !strings.Contains(r.Text, "Last failure") {
105
+ t.Fatalf("state = %+v report = %+v", s, r)
106
+ }
107
+ })
108
+ tw(t, "auth", "a stored key readable by other users is reported as unusable, never as enabled", func(t *testing.T) {
109
+ if runtime.GOOS == "windows" {
110
+ t.Skip("POSIX permission bits")
111
+ }
112
+ isolate(t)
113
+ writeFile(t, CredentialsPath(), `{"apiKey":"stored-key-0123456789abcdef"}`, 0o644)
114
+ s := mustAuth(t, nil)
115
+ if s.Kind != KeyUnusable || s.Usable || s.Reason == "" || mustDescribe(t, s).Level != LevelError {
116
+ t.Fatalf("state = %+v", s)
117
+ }
118
+ })
119
+ tw(t, "auth", "a corrupt auth record is ignored and clearing forgets both facts", func(t *testing.T) {
120
+ isolate(t)
121
+ writeFile(t, AuthStatePath(), "{ not json", 0o600)
122
+ if s := mustAuth(t, nil); s.Verified || s.LastFailure != nil {
123
+ t.Fatalf("corrupt = %+v", s)
124
+ }
125
+ RecordAuthVerified(time.Date(2026, 1, 4, 0, 0, 0, 0, time.UTC))
126
+ ClearAuthState()
127
+ if s := mustAuth(t, nil); s.Verified || s.LastFailure != nil {
128
+ t.Fatalf("cleared = %+v", s)
129
+ }
130
+ })
131
+ }