@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,262 @@
|
|
|
1
|
+
package pitypesafe
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"encoding/json"
|
|
5
|
+
"math"
|
|
6
|
+
"reflect"
|
|
7
|
+
"strings"
|
|
8
|
+
"testing"
|
|
9
|
+
|
|
10
|
+
"github.com/MichaelKinsy/pigpen/components/typesafe/libraries/typesafe"
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
const secret = "private-user-content"
|
|
14
|
+
|
|
15
|
+
func nearMiss(t *testing.T) any {
|
|
16
|
+
return mustTree(t, `{"state":"synthetic","questions":{
|
|
17
|
+
"team":{"type":"choice","instructions":"Which team?","options":{"billing":"Charges and payments","other":"None of these"}},
|
|
18
|
+
"refund":{"type":"noul","instructions":"Is a refund requested?","criteria":"The sender asks for money back"},
|
|
19
|
+
"severity":{"type":"score","instructions":"How severe?","levels":["Cosmetic","Blocking"]},
|
|
20
|
+
"picks":{"type":"choice","instructions":"Pick one","choices":["a","b"]}}}`)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
func criteriaOf(t *testing.T, r *Request, id string) string {
|
|
24
|
+
t.Helper()
|
|
25
|
+
q, _ := r.Questions.Get(id)
|
|
26
|
+
c, _ := q.(*Object).Get("criteria")
|
|
27
|
+
out, err := EncodeJSON(c)
|
|
28
|
+
if err != nil {
|
|
29
|
+
t.Fatal(err)
|
|
30
|
+
}
|
|
31
|
+
return string(out)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
func TestSchema(t *testing.T) {
|
|
35
|
+
tw(t, "schema", "admission accepts the near-miss aliases the agent tool has always accepted", func(t *testing.T) {
|
|
36
|
+
p, err := PrepareEvaluationRequest(nearMiss(t), PrepareOptions{})
|
|
37
|
+
if err != nil {
|
|
38
|
+
t.Fatal(err)
|
|
39
|
+
}
|
|
40
|
+
want := map[string]string{"team": `{"billing":"Charges and payments","other":"None of these"}`, "refund": `{"true":"The sender asks for money back"}`, "severity": `["Cosmetic","Blocking"]`, "picks": `{"a":null,"b":null}`}
|
|
41
|
+
for id, w := range want {
|
|
42
|
+
if got := criteriaOf(t, p, id); got != w {
|
|
43
|
+
t.Errorf("%s criteria = %s, want %s", id, got, w)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
})
|
|
47
|
+
tw(t, "schema", "admission normalizes before it validates", func(t *testing.T) {
|
|
48
|
+
v := mustTree(t, `{"state":"s","questions":{"q":{"type":"choice","criteria":["a","b"]}}}`)
|
|
49
|
+
if _, err := ParseEvaluationRequest(v); !hasCode(err, CodeValidation) {
|
|
50
|
+
t.Fatalf("parse alone must reject: %v", err)
|
|
51
|
+
}
|
|
52
|
+
if _, err := PrepareEvaluationRequest(v, PrepareOptions{}); err != nil {
|
|
53
|
+
t.Fatalf("prepare must accept: %v", err)
|
|
54
|
+
}
|
|
55
|
+
})
|
|
56
|
+
tw(t, "schema", "normalization is idempotent", func(t *testing.T) {
|
|
57
|
+
once := NormalizeEvaluationRequest(nearMiss(t))
|
|
58
|
+
twice := NormalizeEvaluationRequest(once)
|
|
59
|
+
a, _ := EncodeJSON(once)
|
|
60
|
+
b, _ := EncodeJSON(twice)
|
|
61
|
+
if string(a) != string(b) {
|
|
62
|
+
t.Fatalf("%s != %s", a, b)
|
|
63
|
+
}
|
|
64
|
+
if _, err := PrepareEvaluationRequest(once, PrepareOptions{}); err != nil {
|
|
65
|
+
t.Fatal(err)
|
|
66
|
+
}
|
|
67
|
+
})
|
|
68
|
+
tw(t, "schema", "admission enforces the byte budget on the serialized request", func(t *testing.T) {
|
|
69
|
+
v := mustTree(t, `{"state":"`+strings.Repeat("🙂", 50)+`","questions":{"yes":{"type":"noul","instructions":"Is this synthetic?"}}}`)
|
|
70
|
+
body, _ := EncodeJSON(v)
|
|
71
|
+
n := len(body)
|
|
72
|
+
if n >= DefaultMaxInputBytes {
|
|
73
|
+
t.Fatal("fixture too large")
|
|
74
|
+
}
|
|
75
|
+
if _, err := PrepareEvaluationRequest(v, PrepareOptions{MaxInputBytes: n - 1}); !hasCode(err, CodeValidation) {
|
|
76
|
+
t.Fatalf("one byte under the limit must fail: %v", err)
|
|
77
|
+
}
|
|
78
|
+
if _, err := PrepareEvaluationRequest(v, PrepareOptions{MaxInputBytes: n}); err != nil {
|
|
79
|
+
t.Fatalf("exactly the limit must pass: %v", err)
|
|
80
|
+
}
|
|
81
|
+
})
|
|
82
|
+
tw(t, "schema", "the default byte budget is 64 KiB", func(t *testing.T) {
|
|
83
|
+
v := mustTree(t, `{"state":"`+strings.Repeat("x", DefaultMaxInputBytes)+`","questions":{"yes":{"type":"noul","instructions":"?"}}}`)
|
|
84
|
+
if DefaultMaxInputBytes != 65536 {
|
|
85
|
+
t.Fatal("default is not 64 KiB")
|
|
86
|
+
}
|
|
87
|
+
if _, err := PrepareEvaluationRequest(v, PrepareOptions{}); !hasCode(err, CodeValidation) {
|
|
88
|
+
t.Fatalf("oversized request must fail: %v", err)
|
|
89
|
+
}
|
|
90
|
+
if _, err := PrepareEvaluationRequest(v, PrepareOptions{MaxInputBytes: DefaultMaxInputBytes * 2}); err != nil {
|
|
91
|
+
t.Fatal(err)
|
|
92
|
+
}
|
|
93
|
+
})
|
|
94
|
+
tw(t, "schema", "admission still rejects non-JSON state", func(t *testing.T) {
|
|
95
|
+
cyc := map[string]any{}
|
|
96
|
+
cyc["self"] = cyc
|
|
97
|
+
for name, state := range map[string]any{"cycle": cyc, "nan": map[string]any{"n": math.NaN()}} {
|
|
98
|
+
_, err := PrepareEvaluationRequest(map[string]any{"state": state, "questions": map[string]any{"yes": map[string]any{"type": "noul", "instructions": "?"}}}, PrepareOptions{})
|
|
99
|
+
if !hasCode(err, CodeValidation) {
|
|
100
|
+
t.Errorf("%s: %v", name, err)
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
})
|
|
104
|
+
tw(t, "schema", "every field the agent authors carries a description, so a bare union is not its only guidance", func(t *testing.T) {
|
|
105
|
+
data, _ := json.Marshal(EvaluationSchema())
|
|
106
|
+
var schema any
|
|
107
|
+
_ = json.Unmarshal(data, &schema)
|
|
108
|
+
authored := map[string]int{}
|
|
109
|
+
var undescribed []string
|
|
110
|
+
var walk func(any)
|
|
111
|
+
walk = func(node any) {
|
|
112
|
+
switch n := node.(type) {
|
|
113
|
+
case []any:
|
|
114
|
+
for _, e := range n {
|
|
115
|
+
walk(e)
|
|
116
|
+
}
|
|
117
|
+
case map[string]any:
|
|
118
|
+
if props, ok := n["properties"].(map[string]any); ok {
|
|
119
|
+
for name, f := range props {
|
|
120
|
+
authored[name]++
|
|
121
|
+
if d, _ := f.(map[string]any)["description"].(string); d == "" {
|
|
122
|
+
undescribed = append(undescribed, name)
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
for _, v := range n {
|
|
127
|
+
walk(v)
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
walk(schema)
|
|
132
|
+
if authored["type"] != 3 {
|
|
133
|
+
t.Errorf("the schema must still offer noul, choice, and score: %d", authored["type"])
|
|
134
|
+
}
|
|
135
|
+
for _, name := range []string{"state", "questions", "model", "instructions", "criteria"} {
|
|
136
|
+
if authored[name] == 0 {
|
|
137
|
+
t.Errorf("%s is no longer authored", name)
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
for _, name := range undescribed {
|
|
141
|
+
// `true` and `false` inside noul criteria are the only fields whose parent already explains them.
|
|
142
|
+
if name != "true" && name != "false" {
|
|
143
|
+
t.Errorf("field %q has no description", name)
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
})
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// The cases below are the client.test.ts cases that exercise admission and need no network.
|
|
150
|
+
func TestClientAdmission(t *testing.T) {
|
|
151
|
+
tw(t, "client", "structured state, descriptions, and null values are supported", func(t *testing.T) {
|
|
152
|
+
v := typesafe.SystemOneRequest{State: typesafe.Null, Questions: typesafe.Questions{
|
|
153
|
+
typesafe.Ask("yes", typesafe.Noul(map[string]any{"goal": "Check"}).Yes([]any{"a"}).No(nil)),
|
|
154
|
+
typesafe.Ask("pick", typesafe.Choice([]any{"Choose"}, typesafe.Opt("a", map[string]any{"detail": "a"}), typesafe.Opt("b", nil))),
|
|
155
|
+
typesafe.Ask("rating", typesafe.Score(nil, nil, map[string]any{"level": "high"})),
|
|
156
|
+
}}
|
|
157
|
+
if _, err := ParseEvaluationRequest(v); err != nil {
|
|
158
|
+
t.Fatal(err)
|
|
159
|
+
}
|
|
160
|
+
})
|
|
161
|
+
tw(t, "client", "validation errors name the offending path, never the submitted value", func(t *testing.T) {
|
|
162
|
+
cases := []struct {
|
|
163
|
+
request string
|
|
164
|
+
path string
|
|
165
|
+
}{
|
|
166
|
+
{`{"state":"` + secret + `","questions":{"q":{"type":"choice","instructions":"` + secret + `","criteria":["a",1]}}}`, `questions\.q`},
|
|
167
|
+
{`{"state":"` + secret + `","questions":{"q":{"type":"score","criteria":["` + secret + `"]}}}`, `questions\.q`},
|
|
168
|
+
{`{"state":"` + secret + `","questions":{}}`, `questions`},
|
|
169
|
+
{`{"state":"` + secret + `"}`, `questions|request`},
|
|
170
|
+
{`{"state":"` + secret + `","questions":{"q":{"type":"noul","instructions":"ok"}},"extra":"` + secret + `"}`, `extra`},
|
|
171
|
+
}
|
|
172
|
+
for _, c := range cases {
|
|
173
|
+
_, err := ParseEvaluationRequest(mustTree(t, c.request))
|
|
174
|
+
if !hasCode(err, CodeValidation) {
|
|
175
|
+
t.Fatalf("%s: %v", c.request, err)
|
|
176
|
+
}
|
|
177
|
+
msg := err.Error()
|
|
178
|
+
if !regexpMatch(c.path, msg) || !strings.Contains(msg, "Expected { state, questions") || strings.Contains(msg, secret) {
|
|
179
|
+
t.Errorf("message %q does not name %s, carry the usage, or leaks the value", msg, c.path)
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
})
|
|
183
|
+
tw(t, "client", "normalization accepts common model near-misses without loosening the schema", func(t *testing.T) {
|
|
184
|
+
v := mustTree(t, `{"state":"s","questions":{
|
|
185
|
+
"team":{"type":"choice","instructions":"Which team?","options":["frontend","backend"]},
|
|
186
|
+
"level":{"type":"score","instructions":"How bad?","levels":["fine","bad"]},
|
|
187
|
+
"flag":{"type":"noul","instructions":"Is it?","criteria":"Yes when stated"},
|
|
188
|
+
"keep":{"type":"choice","instructions":"Already valid","criteria":{"a":"A","b":null}}}}`)
|
|
189
|
+
r, err := ParseEvaluationRequest(NormalizeEvaluationRequest(v))
|
|
190
|
+
if err != nil {
|
|
191
|
+
t.Fatal(err)
|
|
192
|
+
}
|
|
193
|
+
q := func(id string) string { x, _ := r.Questions.Get(id); b, _ := EncodeJSON(x); return string(b) }
|
|
194
|
+
want := map[string]string{
|
|
195
|
+
"team": `{"type":"choice","instructions":"Which team?","criteria":{"frontend":null,"backend":null}}`,
|
|
196
|
+
"level": `{"type":"score","instructions":"How bad?","criteria":["fine","bad"]}`,
|
|
197
|
+
"flag": `{"type":"noul","instructions":"Is it?","criteria":{"true":"Yes when stated"}}`,
|
|
198
|
+
"keep": `{"type":"choice","instructions":"Already valid","criteria":{"a":"A","b":null}}`,
|
|
199
|
+
}
|
|
200
|
+
for id, w := range want {
|
|
201
|
+
if q(id) != w {
|
|
202
|
+
t.Errorf("%s = %s, want %s", id, q(id), w)
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
for _, in := range []any{nil, "text", []any{}, mustTree(t, `{"questions":[]}`), mustTree(t, `{"questions":{"q":"text"}}`)} {
|
|
206
|
+
out := NormalizeEvaluationRequest(in)
|
|
207
|
+
a, _ := EncodeJSON(in)
|
|
208
|
+
b, _ := EncodeJSON(out)
|
|
209
|
+
if string(a) != string(b) {
|
|
210
|
+
t.Errorf("normalize(%s) = %s", a, b)
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
bad := mustTree(t, `{"state":"s","questions":{"q":{"type":"choice","options":["a",2]}}}`)
|
|
214
|
+
if _, err := ParseEvaluationRequest(NormalizeEvaluationRequest(bad)); !hasCode(err, CodeValidation) {
|
|
215
|
+
t.Fatalf("a non-string label must stay invalid: %v", err)
|
|
216
|
+
}
|
|
217
|
+
})
|
|
218
|
+
tw(t, "client", "non-JSON state, getters, cycles, and excessive nesting are rejected", func(t *testing.T) {
|
|
219
|
+
// Getters, Date and BigInt have no Go counterpart: a Go value either marshals to JSON or it does not.
|
|
220
|
+
cycle := map[string]any{}
|
|
221
|
+
cycle["self"] = cycle
|
|
222
|
+
var nested any
|
|
223
|
+
for i := 0; i < 70; i++ {
|
|
224
|
+
nested = map[string]any{"nested": nested}
|
|
225
|
+
}
|
|
226
|
+
invalid := []any{cycle, map[string]any{"n": math.NaN()}, map[string]any{"n": math.Inf(1)}, map[string]any{"fn": func() {}}, map[string]any{"c": make(chan int)}, nested}
|
|
227
|
+
questions := map[string]any{"yes": map[string]any{"type": "noul", "instructions": "?"}}
|
|
228
|
+
for i, state := range invalid {
|
|
229
|
+
if _, err := ParseEvaluationRequest(map[string]any{"state": state, "questions": questions}); !hasCode(err, CodeValidation) {
|
|
230
|
+
t.Errorf("case %d: %v", i, err)
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
})
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
func TestRequestTypedKeepsOrder(t *testing.T) {
|
|
237
|
+
r, err := ParseEvaluationRequest(mustTree(t, `{"state":{"b":1,"a":2},"questions":{"z":{"type":"noul"},"a":{"type":"choice","criteria":{"y":null,"x":"d"}}},"model":"m"}`))
|
|
238
|
+
if err != nil {
|
|
239
|
+
t.Fatal(err)
|
|
240
|
+
}
|
|
241
|
+
typed, err := r.Typed()
|
|
242
|
+
if err != nil {
|
|
243
|
+
t.Fatal(err)
|
|
244
|
+
}
|
|
245
|
+
if len(typed.Questions) != 2 || typed.Questions[0].Name != "z" || typed.Questions[1].Name != "a" || typed.Model != "m" {
|
|
246
|
+
t.Fatalf("typed = %+v", typed)
|
|
247
|
+
}
|
|
248
|
+
body, _ := json.Marshal(typed)
|
|
249
|
+
if !strings.Contains(string(body), `"b":1,"a":2`) || !strings.Contains(string(body), `"y":null,"x":"d"`) {
|
|
250
|
+
t.Fatalf("order lost: %s", body)
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
func TestTreeQuestionOrderSurvivesAdmission(t *testing.T) {
|
|
255
|
+
r, err := PrepareEvaluationRequest(mustTree(t, `{"state":"s","questions":{"z":{"type":"noul"},"a":{"type":"noul"}}}`), PrepareOptions{})
|
|
256
|
+
if err != nil {
|
|
257
|
+
t.Fatal(err)
|
|
258
|
+
}
|
|
259
|
+
if got := r.Questions.Keys(); !reflect.DeepEqual(got, []string{"z", "a"}) {
|
|
260
|
+
t.Fatalf("keys = %v", got)
|
|
261
|
+
}
|
|
262
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { parseEvaluationRequest } from "./src/schema.ts";
|
|
3
|
+
const cases = JSON.parse(readFileSync("battery.json","utf8"));
|
|
4
|
+
const out = cases.map((c: unknown) => { try { parseEvaluationRequest(c); return "OK"; } catch (e) { return (e as Error).message.replace(/ Expected \{.*$/, ""); } });
|
|
5
|
+
console.log(JSON.stringify(out, null, 1));
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"request": {
|
|
4
|
+
"state": "s"
|
|
5
|
+
},
|
|
6
|
+
"message": "Invalid evaluation request at request: must have required properties questions.",
|
|
7
|
+
"compared": true
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"request": {
|
|
11
|
+
"questions": {}
|
|
12
|
+
},
|
|
13
|
+
"message": "Invalid evaluation request at request: must have required properties state; questions: must not have fewer than 1 properties.",
|
|
14
|
+
"compared": true
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"request": {
|
|
18
|
+
"state": "s",
|
|
19
|
+
"questions": {}
|
|
20
|
+
},
|
|
21
|
+
"message": "Invalid evaluation request at questions: must not have fewer than 1 properties.",
|
|
22
|
+
"compared": true
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"request": {
|
|
26
|
+
"state": 5,
|
|
27
|
+
"questions": {
|
|
28
|
+
"q": {
|
|
29
|
+
"type": "noul"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"message": "Invalid evaluation request at state: must be string; state: must be null; state: must be array.",
|
|
34
|
+
"compared": true
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"request": {
|
|
38
|
+
"state": "s",
|
|
39
|
+
"questions": []
|
|
40
|
+
},
|
|
41
|
+
"message": "Invalid evaluation request at questions: must be object.",
|
|
42
|
+
"compared": true
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"request": {
|
|
46
|
+
"state": "s",
|
|
47
|
+
"questions": {
|
|
48
|
+
"q": "x"
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
"message": "Invalid evaluation request at questions.q: must be object; questions.q: must be object; questions.q: must be object.",
|
|
52
|
+
"compared": true
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"request": {
|
|
56
|
+
"state": "s",
|
|
57
|
+
"questions": {
|
|
58
|
+
"q": {
|
|
59
|
+
"type": "bad"
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
"message": "Invalid evaluation request at questions.q.type: must be equal to constant; questions.q: must have required properties criteria; questions.q.type: must be equal to constant.",
|
|
64
|
+
"compared": false
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
"request": {
|
|
68
|
+
"state": "s",
|
|
69
|
+
"questions": {
|
|
70
|
+
"q": {
|
|
71
|
+
"instructions": "x"
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
"message": "Invalid evaluation request at questions.q: must have required properties type; questions.q: must have required properties type, criteria; questions.q: must have required properties type, criteria.",
|
|
76
|
+
"compared": false
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
"request": {
|
|
80
|
+
"state": "s",
|
|
81
|
+
"questions": {
|
|
82
|
+
"q": {
|
|
83
|
+
"type": "noul",
|
|
84
|
+
"extra": 1
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
"message": "Invalid evaluation request at questions.q.extra: schema is false; questions.q: must not have additional properties; questions.q: must have required properties criteria.",
|
|
89
|
+
"compared": false
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
"request": {
|
|
93
|
+
"state": "s",
|
|
94
|
+
"questions": {
|
|
95
|
+
"q": {
|
|
96
|
+
"type": "noul",
|
|
97
|
+
"criteria": "x"
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
"message": "Invalid evaluation request at questions.q.criteria: must be null; questions.q.criteria: must be object; questions.q.criteria: must match a schema in anyOf.",
|
|
102
|
+
"compared": false
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
"request": {
|
|
106
|
+
"state": "s",
|
|
107
|
+
"questions": {
|
|
108
|
+
"q": {
|
|
109
|
+
"type": "noul",
|
|
110
|
+
"criteria": {
|
|
111
|
+
"maybe": "x"
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
"message": "Invalid evaluation request at questions.q.criteria: must be null; questions.q.criteria.maybe: schema is false; questions.q.criteria: must not have additional properties.",
|
|
117
|
+
"compared": false
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
"request": {
|
|
121
|
+
"state": "s",
|
|
122
|
+
"questions": {
|
|
123
|
+
"q": {
|
|
124
|
+
"type": "noul",
|
|
125
|
+
"instructions": 5
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
"message": "Invalid evaluation request at questions.q.instructions: must be string; questions.q.instructions: must be null; questions.q.instructions: must be array.",
|
|
130
|
+
"compared": false
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
"request": {
|
|
134
|
+
"state": "s",
|
|
135
|
+
"questions": {
|
|
136
|
+
"q": {
|
|
137
|
+
"type": "choice"
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
"message": "Invalid evaluation request at questions.q.type: must be equal to constant; questions.q: must have required properties criteria; questions.q: must have required properties criteria.",
|
|
142
|
+
"compared": false
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
"request": {
|
|
146
|
+
"state": "s",
|
|
147
|
+
"questions": {
|
|
148
|
+
"q": {
|
|
149
|
+
"type": "choice",
|
|
150
|
+
"criteria": {}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
},
|
|
154
|
+
"message": "Invalid evaluation request at questions.q.type: must be equal to constant; questions.q.criteria: must not have fewer than 1 properties; questions.q.type: must be equal to constant.",
|
|
155
|
+
"compared": false
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
"request": {
|
|
159
|
+
"state": "s",
|
|
160
|
+
"questions": {
|
|
161
|
+
"q": {
|
|
162
|
+
"type": "choice",
|
|
163
|
+
"criteria": [
|
|
164
|
+
"a"
|
|
165
|
+
]
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
"message": "Invalid evaluation request at questions.q.type: must be equal to constant; questions.q.criteria: must be null; questions.q.criteria: must be object.",
|
|
170
|
+
"compared": false
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
"request": {
|
|
174
|
+
"state": "s",
|
|
175
|
+
"questions": {
|
|
176
|
+
"q": {
|
|
177
|
+
"type": "choice",
|
|
178
|
+
"criteria": {
|
|
179
|
+
"a": 5
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
},
|
|
184
|
+
"message": "Invalid evaluation request at questions.q.type: must be equal to constant; questions.q.criteria: must be null; questions.q.criteria.a: schema is false.",
|
|
185
|
+
"compared": false
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
"request": {
|
|
189
|
+
"state": "s",
|
|
190
|
+
"questions": {
|
|
191
|
+
"q": {
|
|
192
|
+
"type": "score",
|
|
193
|
+
"criteria": [
|
|
194
|
+
"a"
|
|
195
|
+
]
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
},
|
|
199
|
+
"message": "Invalid evaluation request at questions.q.type: must be equal to constant; questions.q.criteria: must be null; questions.q.criteria: must be object.",
|
|
200
|
+
"compared": false
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
"request": {
|
|
204
|
+
"state": "s",
|
|
205
|
+
"questions": {
|
|
206
|
+
"q": {
|
|
207
|
+
"type": "score",
|
|
208
|
+
"criteria": "x"
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
},
|
|
212
|
+
"message": "Invalid evaluation request at questions.q.type: must be equal to constant; questions.q.criteria: must be null; questions.q.criteria: must be object.",
|
|
213
|
+
"compared": false
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
"request": {
|
|
217
|
+
"state": "s",
|
|
218
|
+
"questions": {
|
|
219
|
+
"q": {
|
|
220
|
+
"type": "score",
|
|
221
|
+
"criteria": [
|
|
222
|
+
"a",
|
|
223
|
+
5
|
|
224
|
+
]
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
},
|
|
228
|
+
"message": "Invalid evaluation request at questions.q.type: must be equal to constant; questions.q.criteria: must be null; questions.q.criteria: must be object.",
|
|
229
|
+
"compared": false
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
"request": {
|
|
233
|
+
"state": "s",
|
|
234
|
+
"questions": {
|
|
235
|
+
"q": {
|
|
236
|
+
"type": "noul"
|
|
237
|
+
}
|
|
238
|
+
},
|
|
239
|
+
"model": ""
|
|
240
|
+
},
|
|
241
|
+
"message": "Invalid evaluation request at model: must not have fewer than 1 characters.",
|
|
242
|
+
"compared": true
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
"request": {
|
|
246
|
+
"state": "s",
|
|
247
|
+
"questions": {
|
|
248
|
+
"q": {
|
|
249
|
+
"type": "noul"
|
|
250
|
+
}
|
|
251
|
+
},
|
|
252
|
+
"model": 5
|
|
253
|
+
},
|
|
254
|
+
"message": "Invalid evaluation request at model: must be string.",
|
|
255
|
+
"compared": true
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
"request": {
|
|
259
|
+
"state": "s",
|
|
260
|
+
"questions": {
|
|
261
|
+
"q": {
|
|
262
|
+
"type": "noul"
|
|
263
|
+
}
|
|
264
|
+
},
|
|
265
|
+
"extra": 1
|
|
266
|
+
},
|
|
267
|
+
"message": "Invalid evaluation request at extra: schema is false; request: must not have additional properties.",
|
|
268
|
+
"compared": true
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
"request": [],
|
|
272
|
+
"message": "Invalid evaluation request at request: must be object.",
|
|
273
|
+
"compared": true
|
|
274
|
+
},
|
|
275
|
+
{
|
|
276
|
+
"request": "x",
|
|
277
|
+
"message": "Invalid evaluation request at request: must be object.",
|
|
278
|
+
"compared": true
|
|
279
|
+
},
|
|
280
|
+
{
|
|
281
|
+
"request": null,
|
|
282
|
+
"message": "Invalid evaluation request at request: must be object.",
|
|
283
|
+
"compared": true
|
|
284
|
+
}
|
|
285
|
+
]
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
package pitypesafe
|
|
2
|
+
|
|
3
|
+
// Twin helpers for a port whose original has tests. Every upstream test case gets one
|
|
4
|
+
// tw(...) call carrying the upstream title unchanged, or one tskip(...) with the reason it
|
|
5
|
+
// cannot be ported. `pigeq twins check` reads these calls and fails on a missing title, an
|
|
6
|
+
// unknown one (a misspelling) or one reason shared by many skips.
|
|
7
|
+
//
|
|
8
|
+
// pigeq twins list --tests <upstream>/test > port/upstream-tests.json
|
|
9
|
+
// pigeq twins check --ledger port/upstream-tests.json --go extensions/<name> [--files a,b]
|
|
10
|
+
//
|
|
11
|
+
// Copy this file into the port as twin_test.go and replace pitypesafe with the package name (in
|
|
12
|
+
// an external test package use `pitypesafe_test`). A parametrized upstream title (it contains
|
|
13
|
+
// `${`) is twinned once, with the template text unchanged.
|
|
14
|
+
|
|
15
|
+
import "testing"
|
|
16
|
+
|
|
17
|
+
// tw runs one exact twin: file is the upstream test file (its name without .test.mjs), title
|
|
18
|
+
// the upstream test title, unchanged.
|
|
19
|
+
func tw(t *testing.T, file, title string, fn func(t *testing.T)) {
|
|
20
|
+
t.Helper()
|
|
21
|
+
t.Run(title, fn)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// tskip records a named gap: the upstream case has no Go twin, with its own reason.
|
|
25
|
+
func tskip(t *testing.T, file, title, reason string) {
|
|
26
|
+
t.Helper()
|
|
27
|
+
t.Run(title, func(t *testing.T) { t.Skip(reason) })
|
|
28
|
+
}
|