@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.
- package/CREDITS.md +22 -0
- package/LICENSE +22 -0
- package/README.md +237 -0
- package/extensions/jev/ask.go +166 -0
- package/extensions/jev/ask_test.go +218 -0
- package/extensions/jev/backend.go +128 -0
- package/extensions/jev/bench_test.go +64 -0
- package/extensions/jev/boundaries_test.go +159 -0
- package/extensions/jev/command.go +224 -0
- package/extensions/jev/commands_test.go +214 -0
- package/extensions/jev/config.go +450 -0
- package/extensions/jev/errors_test.go +191 -0
- package/extensions/jev/extension.go +391 -0
- package/extensions/jev/fakehost_test.go +548 -0
- package/extensions/jev/gate.go +125 -0
- package/extensions/jev/gate_test.go +610 -0
- package/extensions/jev/gatekey_test.go +24 -0
- package/extensions/jev/go.mod +9 -0
- package/extensions/jev/go.sum +2 -0
- package/extensions/jev/go.work +10 -0
- package/extensions/jev/helpers_test.go +404 -0
- package/extensions/jev/memo.go +88 -0
- package/extensions/jev/output.go +89 -0
- package/extensions/jev/output_test.go +187 -0
- package/extensions/jev/ownmodel_test.go +118 -0
- package/extensions/jev/render.go +136 -0
- package/extensions/jev/review_test.go +310 -0
- package/extensions/jev/source_test.go +57 -0
- package/extensions/jev/text.go +174 -0
- package/extensions/jev/trust_test.go +335 -0
- package/extensions/jev/types.go +227 -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 +107 -0
- package/port/e2e/gate-and-output.py +35 -0
- package/port/e2e/jev-ask.py +36 -0
- package/port/e2e/model-switch.py +44 -0
- package/port/e2e/off-by-default.py +34 -0
- package/port/gen-scenarios.py +103 -0
- package/port/golden/cache-identical-calls.jsonl +30 -0
- package/port/golden/clear.jsonl +22 -0
- package/port/golden/commands.jsonl +43 -0
- package/port/golden/enforce-accept.jsonl +23 -0
- package/port/golden/enforce-decline.jsonl +22 -0
- package/port/golden/jev-ask.jsonl +20 -0
- package/port/golden/output-advice.jsonl +23 -0
- package/port/golden/output-leak.jsonl +24 -0
- package/port/golden/output-low-confidence.jsonl +22 -0
- package/port/golden/shadow-flagged.jsonl +23 -0
- package/port/golden/unjudged-tools.jsonl +19 -0
- package/port/golden/write-elision.jsonl +21 -0
- package/port/mutate-unit.py +63 -0
- package/port/mutations.json +578 -0
- package/port/oracle/LICENSE +21 -0
- package/port/oracle/README.md +181 -0
- package/port/oracle/SHA256SUMS +8 -0
- package/port/oracle/package.json +43 -0
- package/port/oracle/src/client.ts +409 -0
- package/port/oracle/src/config.ts +363 -0
- package/port/oracle/src/gate.ts +229 -0
- package/port/oracle/src/index.ts +649 -0
- package/port/oracle/src/output.ts +163 -0
- package/port/red-run.log +309 -0
- package/port/scenarios/cache-identical-calls.json +71 -0
- package/port/scenarios/clear.json +61 -0
- package/port/scenarios/commands.json +119 -0
- package/port/scenarios/enforce-accept.json +66 -0
- package/port/scenarios/enforce-decline.json +57 -0
- package/port/scenarios/jev-ask.json +83 -0
- package/port/scenarios/output-advice.json +61 -0
- package/port/scenarios/output-leak.json +61 -0
- package/port/scenarios/output-low-confidence.json +61 -0
- package/port/scenarios/shadow-flagged.json +61 -0
- package/port/scenarios/unjudged-tools.json +55 -0
- package/port/scenarios/write-elision.json +53 -0
- package/provenance.json +18 -0
|
@@ -0,0 +1,610 @@
|
|
|
1
|
+
package jev_test
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"encoding/json"
|
|
5
|
+
"strconv"
|
|
6
|
+
"strings"
|
|
7
|
+
"sync"
|
|
8
|
+
"testing"
|
|
9
|
+
"time"
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
// Twins of the original's gate behavior (src/index.ts tool_call, src/gate.ts).
|
|
13
|
+
// The original has no tests of its own; each case is derived from a branch of
|
|
14
|
+
// the source and named after it. Upstream file:line are in port/PORT.md.
|
|
15
|
+
|
|
16
|
+
func bash(cmd string) map[string]any { return map[string]any{"command": cmd} }
|
|
17
|
+
|
|
18
|
+
func TestGate_ShadowFlaggedNotifiesAndNeverBlocks(t *testing.T) {
|
|
19
|
+
e := newEnv(t)
|
|
20
|
+
t.Setenv("TYPESAFE_API_KEY", testKey)
|
|
21
|
+
srv := newFakeJev(t, always(flaggedGate()))
|
|
22
|
+
e.writeGlobal(t, jevConfig(srv, nil))
|
|
23
|
+
h := start(t, e, newHostState(), HostOptions{})
|
|
24
|
+
block, _ := h.toolCall("bash", bash("rm -rf src && git push --force origin main"))
|
|
25
|
+
if block {
|
|
26
|
+
t.Fatal("shadow mode blocked a call")
|
|
27
|
+
}
|
|
28
|
+
want := "warning: jev shadow: bash - destructive 0.99, exfiltration 0.79, beyond_scope 0.98, impact 3.00/3 at confidence 0.91"
|
|
29
|
+
if !h.anyNotification(want) {
|
|
30
|
+
t.Errorf("notifications = %q, want %q", h.notifications(), want)
|
|
31
|
+
}
|
|
32
|
+
if got := h.lastStatus(); got != "jev: destructive 0.99, exfiltration 0.79, beyond_scope 0.98, impact 3.00/3 at confidence 0.91" {
|
|
33
|
+
t.Errorf("status = %q", got)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
func TestGate_ClearVerdictSetsClearStatus(t *testing.T) {
|
|
38
|
+
e := newEnv(t)
|
|
39
|
+
t.Setenv("TYPESAFE_API_KEY", testKey)
|
|
40
|
+
srv := newFakeJev(t, always(clearGate()))
|
|
41
|
+
e.writeGlobal(t, jevConfig(srv, nil))
|
|
42
|
+
h := start(t, e, newHostState(), HostOptions{})
|
|
43
|
+
if block, _ := h.toolCall("bash", bash("git status --short")); block {
|
|
44
|
+
t.Fatal("blocked")
|
|
45
|
+
}
|
|
46
|
+
if got := h.lastStatus(); got != "jev: clear (shadow)" {
|
|
47
|
+
t.Errorf("status = %q", got)
|
|
48
|
+
}
|
|
49
|
+
if n := len(h.notifications()); n != 0 {
|
|
50
|
+
t.Errorf("clear verdict notified: %q", h.notifications())
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
func TestGate_SessionStartStatus(t *testing.T) {
|
|
55
|
+
e := newEnv(t)
|
|
56
|
+
t.Setenv("TYPESAFE_API_KEY", testKey)
|
|
57
|
+
srv := newFakeJev(t, always(clearGate()))
|
|
58
|
+
e.writeGlobal(t, jevConfig(srv, map[string]any{"output": map[string]any{"enabled": false}}))
|
|
59
|
+
h := start(t, e, newHostState(), HostOptions{})
|
|
60
|
+
if got := h.lastStatus(); got != "jev: shadow (out off)" {
|
|
61
|
+
t.Errorf("status = %q, want %q", got, "jev: shadow (out off)")
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
func TestGate_EnforceAsksAndDeclineBlocks(t *testing.T) {
|
|
66
|
+
e := newEnv(t)
|
|
67
|
+
t.Setenv("TYPESAFE_API_KEY", testKey)
|
|
68
|
+
srv := newFakeJev(t, always(gateBody(0.99, 0.04, 0.4, 0.5, 0.9)))
|
|
69
|
+
e.writeGlobal(t, jevConfig(srv, map[string]any{"gate": map[string]any{"mode": "enforce"}}))
|
|
70
|
+
hs := newHostState()
|
|
71
|
+
h := start(t, e, hs, HostOptions{})
|
|
72
|
+
block, reason := h.toolCall("bash", bash("rm -rf src"))
|
|
73
|
+
if !block || reason != "pi-jev: destructive 0.99 (declined)" {
|
|
74
|
+
t.Fatalf("block=%v reason=%q", block, reason)
|
|
75
|
+
}
|
|
76
|
+
c := hs.confirmCalls()
|
|
77
|
+
if len(c) != 1 || c[0]["title"] != "Jev flagged this tool call" || c[0]["message"] != "bash\ndestructive 0.99\n\nRun it anyway?" {
|
|
78
|
+
t.Errorf("confirm = %+v", c)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
func TestGate_EnforceAcceptedRuns(t *testing.T) {
|
|
83
|
+
e := newEnv(t)
|
|
84
|
+
t.Setenv("TYPESAFE_API_KEY", testKey)
|
|
85
|
+
srv := newFakeJev(t, always(gateBody(0.99, 0.04, 0.4, 0.5, 0.9)))
|
|
86
|
+
e.writeGlobal(t, jevConfig(srv, map[string]any{"gate": map[string]any{"mode": "enforce"}}))
|
|
87
|
+
hs := newHostState()
|
|
88
|
+
hs.confirm = true
|
|
89
|
+
h := start(t, e, hs, HostOptions{})
|
|
90
|
+
if block, _ := h.toolCall("bash", bash("rm -rf src")); block {
|
|
91
|
+
t.Fatal("blocked after the user allowed it")
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
func TestGate_EnforceHeadlessDegradesToWarning(t *testing.T) {
|
|
96
|
+
e := newEnv(t)
|
|
97
|
+
t.Setenv("TYPESAFE_API_KEY", testKey)
|
|
98
|
+
srv := newFakeJev(t, always(gateBody(0.99, 0.04, 0.4, 0.5, 0.9)))
|
|
99
|
+
e.writeGlobal(t, jevConfig(srv, map[string]any{"gate": map[string]any{"mode": "enforce"}}))
|
|
100
|
+
no := false
|
|
101
|
+
hs := newHostState()
|
|
102
|
+
h := start(t, e, hs, HostOptions{Mode: "print", HasUI: &no})
|
|
103
|
+
if block, _ := h.toolCall("bash", bash("rm -rf src")); block {
|
|
104
|
+
t.Fatal("headless enforce blocked without blockWithoutUI")
|
|
105
|
+
}
|
|
106
|
+
if !h.anyNotification("warning: jev: bash - destructive 0.99 (headless: not blocking; set gate.blockWithoutUI to block)") {
|
|
107
|
+
t.Errorf("notifications = %q", h.notifications())
|
|
108
|
+
}
|
|
109
|
+
if len(hs.confirmCalls()) != 0 {
|
|
110
|
+
t.Error("asked for confirmation with no UI")
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
func TestGate_EnforceHeadlessBlocksWithBlockWithoutUI(t *testing.T) {
|
|
115
|
+
e := newEnv(t)
|
|
116
|
+
t.Setenv("TYPESAFE_API_KEY", testKey)
|
|
117
|
+
srv := newFakeJev(t, always(gateBody(0.99, 0.04, 0.4, 0.5, 0.9)))
|
|
118
|
+
e.writeGlobal(t, jevConfig(srv, map[string]any{"gate": map[string]any{"mode": "enforce", "blockWithoutUI": true}}))
|
|
119
|
+
no := false
|
|
120
|
+
h := start(t, e, newHostState(), HostOptions{Mode: "print", HasUI: &no})
|
|
121
|
+
block, reason := h.toolCall("bash", bash("rm -rf src"))
|
|
122
|
+
if !block || reason != "pi-jev: destructive 0.99" {
|
|
123
|
+
t.Fatalf("block=%v reason=%q", block, reason)
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
func TestGate_OnlyJudgesConfiguredTools(t *testing.T) {
|
|
128
|
+
e := newEnv(t)
|
|
129
|
+
t.Setenv("TYPESAFE_API_KEY", testKey)
|
|
130
|
+
srv := newFakeJev(t, always(clearGate()))
|
|
131
|
+
e.writeGlobal(t, jevConfig(srv, nil))
|
|
132
|
+
h := start(t, e, newHostState(), HostOptions{})
|
|
133
|
+
h.toolCall("read", map[string]any{"path": "a.txt"})
|
|
134
|
+
if srv.count() != 0 {
|
|
135
|
+
t.Fatalf("judged read: %d requests", srv.count())
|
|
136
|
+
}
|
|
137
|
+
for _, tool := range []string{"bash", "write", "edit"} {
|
|
138
|
+
h.toolCall(tool, map[string]any{"x": tool})
|
|
139
|
+
}
|
|
140
|
+
if srv.count() != 3 {
|
|
141
|
+
t.Fatalf("requests = %d, want 3 (bash, write, edit)", srv.count())
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
func TestGate_ConfiguredToolsReplaceDefaults(t *testing.T) {
|
|
146
|
+
e := newEnv(t)
|
|
147
|
+
t.Setenv("TYPESAFE_API_KEY", testKey)
|
|
148
|
+
srv := newFakeJev(t, always(clearGate()))
|
|
149
|
+
e.writeGlobal(t, jevConfig(srv, map[string]any{"gate": map[string]any{"tools": []any{"bash"}}}))
|
|
150
|
+
h := start(t, e, newHostState(), HostOptions{})
|
|
151
|
+
h.toolCall("write", map[string]any{"path": "a", "content": "b"})
|
|
152
|
+
if srv.count() != 0 {
|
|
153
|
+
t.Fatal("judged write with gate.tools = [bash]")
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
func TestGate_RequestShape(t *testing.T) {
|
|
158
|
+
e := newEnv(t)
|
|
159
|
+
t.Setenv("TYPESAFE_API_KEY", testKey)
|
|
160
|
+
srv := newFakeJev(t, always(clearGate()))
|
|
161
|
+
e.writeGlobal(t, jevConfig(srv, nil))
|
|
162
|
+
hs := newHostState()
|
|
163
|
+
hs.user = "please list the files"
|
|
164
|
+
h := start(t, e, hs, HostOptions{})
|
|
165
|
+
h.toolCall("bash", bash("ls"))
|
|
166
|
+
reqs := srv.requests()
|
|
167
|
+
if len(reqs) != 1 {
|
|
168
|
+
t.Fatalf("requests = %d", len(reqs))
|
|
169
|
+
}
|
|
170
|
+
r := reqs[0]
|
|
171
|
+
if r.Auth != "Bearer "+testKey {
|
|
172
|
+
t.Errorf("Authorization = %q", r.Auth)
|
|
173
|
+
}
|
|
174
|
+
if r.Path != "/v1/systemone" {
|
|
175
|
+
t.Errorf("path = %q: the endpoint is the API root and the client adds /v1/systemone", r.Path)
|
|
176
|
+
}
|
|
177
|
+
if r.Body["model"] != "jev-test" {
|
|
178
|
+
t.Errorf("model = %v", r.Body["model"])
|
|
179
|
+
}
|
|
180
|
+
st := stateOf(t, r)
|
|
181
|
+
if st["cwd"] != e.cwd || st["tool"] != "bash" || st["user_request"] != "please list the files" {
|
|
182
|
+
t.Errorf("state = %v", st)
|
|
183
|
+
}
|
|
184
|
+
args, _ := st["arguments"].(map[string]any)
|
|
185
|
+
if args["command"] != "ls" {
|
|
186
|
+
t.Errorf("arguments = %v", st["arguments"])
|
|
187
|
+
}
|
|
188
|
+
if p, _ := st["platform"].(string); p == "" {
|
|
189
|
+
t.Error("platform missing")
|
|
190
|
+
}
|
|
191
|
+
qs, _ := r.Body["questions"].(map[string]any)
|
|
192
|
+
for _, id := range []string{"destructive", "exfiltration", "beyond_scope", "impact"} {
|
|
193
|
+
if _, ok := qs[id]; !ok {
|
|
194
|
+
t.Errorf("question %q missing", id)
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
d := qs["destructive"].(map[string]any)
|
|
198
|
+
crit := d["criteria"].(map[string]any)
|
|
199
|
+
if d["type"] != "noul" || d["instructions"] != "Is this action destructive?" ||
|
|
200
|
+
crit["true"] != "Deletes or overwrites data, or rewrites shared history" || crit["false"] != "Nothing is deleted or overwritten" {
|
|
201
|
+
t.Errorf("destructive question = %v", d)
|
|
202
|
+
}
|
|
203
|
+
imp := qs["impact"].(map[string]any)
|
|
204
|
+
lv, _ := imp["criteria"].([]any)
|
|
205
|
+
if imp["type"] != "score" || len(lv) != 4 || lv[3] != "Severe, data loss or a forced overwrite of shared history" {
|
|
206
|
+
t.Errorf("impact question = %v", imp)
|
|
207
|
+
}
|
|
208
|
+
// Question order is part of the wire shape: destructive, exfiltration, beyond_scope, impact.
|
|
209
|
+
order := questionOrder(r.Raw)
|
|
210
|
+
if strings.Join(order, ",") != "destructive,exfiltration,beyond_scope,impact" {
|
|
211
|
+
t.Errorf("question order = %v", order)
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// questionOrder lists the keys of "questions" in wire order.
|
|
216
|
+
func questionOrder(raw string) []string {
|
|
217
|
+
var doc struct {
|
|
218
|
+
Questions json.RawMessage `json:"questions"`
|
|
219
|
+
}
|
|
220
|
+
_ = json.Unmarshal([]byte(raw), &doc)
|
|
221
|
+
dec := json.NewDecoder(strings.NewReader(string(doc.Questions)))
|
|
222
|
+
var keys []string
|
|
223
|
+
dec.Token()
|
|
224
|
+
depth := 0
|
|
225
|
+
for dec.More() || depth > 0 {
|
|
226
|
+
tok, err := dec.Token()
|
|
227
|
+
if err != nil {
|
|
228
|
+
break
|
|
229
|
+
}
|
|
230
|
+
switch v := tok.(type) {
|
|
231
|
+
case json.Delim:
|
|
232
|
+
if v == '{' || v == '[' {
|
|
233
|
+
depth++
|
|
234
|
+
} else {
|
|
235
|
+
depth--
|
|
236
|
+
}
|
|
237
|
+
case string:
|
|
238
|
+
if depth == 0 {
|
|
239
|
+
keys = append(keys, v)
|
|
240
|
+
var skip json.RawMessage
|
|
241
|
+
_ = dec.Decode(&skip)
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return keys
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
func TestGate_ArgumentsKeepInsertionOrder_GAP(t *testing.T) {
|
|
249
|
+
t.Skip("the Go SDK decodes event data into map[string]any, so the original's argument key order is not available; keys are sent sorted. The judgment does not depend on key order; the equivalence harness compares parsed JSON")
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
func TestGate_LongArgumentsAreElided(t *testing.T) {
|
|
253
|
+
e := newEnv(t)
|
|
254
|
+
t.Setenv("TYPESAFE_API_KEY", testKey)
|
|
255
|
+
srv := newFakeJev(t, always(clearGate()))
|
|
256
|
+
e.writeGlobal(t, jevConfig(srv, nil))
|
|
257
|
+
h := start(t, e, newHostState(), HostOptions{})
|
|
258
|
+
body := strings.Repeat("a", 900)
|
|
259
|
+
h.toolCall("write", map[string]any{"path": "f.txt", "content": body})
|
|
260
|
+
args := stateOf(t, srv.first(t))["arguments"].(map[string]any)
|
|
261
|
+
if got, want := args["content"], strings.Repeat("a", 400)+"…[500 chars elided]"; got != want {
|
|
262
|
+
t.Errorf("content = %q", got)
|
|
263
|
+
}
|
|
264
|
+
if args["path"] != "f.txt" {
|
|
265
|
+
t.Errorf("path = %v", args["path"])
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
func TestGate_ArgumentCharsCountUTF16Units(t *testing.T) {
|
|
270
|
+
e := newEnv(t)
|
|
271
|
+
t.Setenv("TYPESAFE_API_KEY", testKey)
|
|
272
|
+
srv := newFakeJev(t, always(clearGate()))
|
|
273
|
+
e.writeGlobal(t, jevConfig(srv, map[string]any{"gate": map[string]any{"argumentChars": 5}}))
|
|
274
|
+
h := start(t, e, newHostState(), HostOptions{})
|
|
275
|
+
// "😀" is two UTF-16 units. "ab😀cd😀" is 8 units: keep 5 = "ab😀c", 3 elided.
|
|
276
|
+
h.toolCall("bash", bash("ab😀cd😀"))
|
|
277
|
+
args := stateOf(t, srv.first(t))["arguments"].(map[string]any)
|
|
278
|
+
if got, want := args["command"], "ab😀c…[3 chars elided]"; got != want {
|
|
279
|
+
t.Errorf("command = %q, want %q", got, want)
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
func TestGate_CutInsideSurrogatePairStaysValidUTF8(t *testing.T) {
|
|
284
|
+
e := newEnv(t)
|
|
285
|
+
t.Setenv("TYPESAFE_API_KEY", testKey)
|
|
286
|
+
srv := newFakeJev(t, always(clearGate()))
|
|
287
|
+
e.writeGlobal(t, jevConfig(srv, map[string]any{"gate": map[string]any{"argumentChars": 3}}))
|
|
288
|
+
h := start(t, e, newHostState(), HostOptions{})
|
|
289
|
+
h.toolCall("bash", bash("ab😀cd")) // cut after 3 units splits the pair
|
|
290
|
+
raw := srv.first(t).Raw
|
|
291
|
+
if !strings.Contains(raw, "…[3 chars elided]") {
|
|
292
|
+
t.Errorf("marker missing: %s", raw)
|
|
293
|
+
}
|
|
294
|
+
var v any
|
|
295
|
+
if err := json.Unmarshal([]byte(raw), &v); err != nil {
|
|
296
|
+
t.Errorf("request is not valid JSON: %v", err)
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// C7: the original stops eliding below depth 4, so a deep string left whole.
|
|
301
|
+
func TestCorrection_DeepStringsAreElidedToo(t *testing.T) {
|
|
302
|
+
e := newEnv(t)
|
|
303
|
+
t.Setenv("TYPESAFE_API_KEY", testKey)
|
|
304
|
+
srv := newFakeJev(t, always(clearGate()))
|
|
305
|
+
e.writeGlobal(t, jevConfig(srv, nil))
|
|
306
|
+
h := start(t, e, newHostState(), HostOptions{})
|
|
307
|
+
secret := strings.Repeat("S", 3000)
|
|
308
|
+
deep := map[string]any{"a": map[string]any{"b": map[string]any{"c": map[string]any{"d": map[string]any{"e": map[string]any{"f": secret}}}}}}
|
|
309
|
+
h.toolCall("bash", deep)
|
|
310
|
+
if strings.Contains(srv.first(t).Raw, strings.Repeat("S", 401)) {
|
|
311
|
+
t.Error("a deeply nested string left the machine whole")
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// C3: maxStateChars is parsed and documented by the original but never applied.
|
|
316
|
+
func TestCorrection_MaxStateCharsCapsTheState(t *testing.T) {
|
|
317
|
+
e := newEnv(t)
|
|
318
|
+
t.Setenv("TYPESAFE_API_KEY", testKey)
|
|
319
|
+
srv := newFakeJev(t, always(clearGate()))
|
|
320
|
+
e.writeGlobal(t, jevConfig(srv, map[string]any{"maxStateChars": 300}))
|
|
321
|
+
hs := newHostState()
|
|
322
|
+
hs.user = strings.Repeat("u", 1200)
|
|
323
|
+
h := start(t, e, hs, HostOptions{})
|
|
324
|
+
in := map[string]any{}
|
|
325
|
+
for i := 0; i < 20; i++ {
|
|
326
|
+
in[string(rune('a'+i))] = strings.Repeat("x", 100)
|
|
327
|
+
}
|
|
328
|
+
h.toolCall("bash", in)
|
|
329
|
+
st := srv.first(t).Body["state"]
|
|
330
|
+
b, _ := json.Marshal(st)
|
|
331
|
+
if n := len([]rune(string(b))); n > 300 {
|
|
332
|
+
t.Errorf("state is %d chars, cap is 300: %s", n, b)
|
|
333
|
+
}
|
|
334
|
+
if s, ok := st.(map[string]any); !ok || s["tool"] != "bash" || s["cwd"] != e.cwd {
|
|
335
|
+
t.Errorf("the cap must keep identifying fields: %s", b)
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
func TestGate_UserRequestIsTruncatedTo1200Units(t *testing.T) {
|
|
340
|
+
e := newEnv(t)
|
|
341
|
+
t.Setenv("TYPESAFE_API_KEY", testKey)
|
|
342
|
+
srv := newFakeJev(t, always(clearGate()))
|
|
343
|
+
e.writeGlobal(t, jevConfig(srv, nil))
|
|
344
|
+
hs := newHostState()
|
|
345
|
+
hs.user = strings.Repeat("u", 1300)
|
|
346
|
+
h := start(t, e, hs, HostOptions{})
|
|
347
|
+
h.toolCall("bash", bash("ls"))
|
|
348
|
+
if got, want := stateOf(t, srv.first(t))["user_request"], strings.Repeat("u", 1200)+"…[truncated]"; got != want {
|
|
349
|
+
t.Errorf("user_request = %v", got)
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
func TestGate_NoUserRequestOmitsTheField(t *testing.T) {
|
|
354
|
+
e := newEnv(t)
|
|
355
|
+
t.Setenv("TYPESAFE_API_KEY", testKey)
|
|
356
|
+
srv := newFakeJev(t, always(clearGate()))
|
|
357
|
+
e.writeGlobal(t, jevConfig(srv, nil))
|
|
358
|
+
h := start(t, e, newHostState(), HostOptions{})
|
|
359
|
+
h.toolCall("bash", bash("ls"))
|
|
360
|
+
if _, ok := stateOf(t, srv.first(t))["user_request"]; ok {
|
|
361
|
+
t.Error("user_request present with no user message")
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
func TestGate_ImpactBelowMinConfidenceDoesNotFlag(t *testing.T) {
|
|
366
|
+
e := newEnv(t)
|
|
367
|
+
t.Setenv("TYPESAFE_API_KEY", testKey)
|
|
368
|
+
srv := newFakeJev(t, always(gateBody(0.1, 0.1, 0.1, 3.0, 0.4)))
|
|
369
|
+
e.writeGlobal(t, jevConfig(srv, nil))
|
|
370
|
+
h := start(t, e, newHostState(), HostOptions{})
|
|
371
|
+
h.toolCall("bash", bash("x"))
|
|
372
|
+
if h.lastStatus() != "jev: clear (shadow)" {
|
|
373
|
+
t.Errorf("status = %q", h.lastStatus())
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
func TestGate_ThresholdIsInclusive(t *testing.T) {
|
|
378
|
+
e := newEnv(t)
|
|
379
|
+
t.Setenv("TYPESAFE_API_KEY", testKey)
|
|
380
|
+
srv := newFakeJev(t, always(gateBody(0.9, 0, 0, 0, 0.9)))
|
|
381
|
+
e.writeGlobal(t, jevConfig(srv, nil))
|
|
382
|
+
h := start(t, e, newHostState(), HostOptions{})
|
|
383
|
+
h.toolCall("bash", bash("x"))
|
|
384
|
+
if h.lastStatus() != "jev: destructive 0.90" {
|
|
385
|
+
t.Errorf("status = %q", h.lastStatus())
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Number semantics: JavaScript toFixed rounds an exact tie up (0.125 -> "0.13").
|
|
390
|
+
func TestGate_ToFixedRoundsTiesUpLikeJavaScript(t *testing.T) {
|
|
391
|
+
e := newEnv(t)
|
|
392
|
+
t.Setenv("TYPESAFE_API_KEY", testKey)
|
|
393
|
+
srv := newFakeJev(t, always(gateBody(0.125, 0, 0, 0, 0.9)))
|
|
394
|
+
e.writeGlobal(t, jevConfig(srv, map[string]any{"gate": map[string]any{"blockOn": map[string]any{"destructive": 0.1}}}))
|
|
395
|
+
h := start(t, e, newHostState(), HostOptions{})
|
|
396
|
+
h.toolCall("bash", bash("x"))
|
|
397
|
+
if h.lastStatus() != "jev: destructive 0.13" {
|
|
398
|
+
t.Errorf("status = %q, want the JavaScript rendering 0.13", h.lastStatus())
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
func TestGate_DisabledGateSkips(t *testing.T) {
|
|
403
|
+
e := newEnv(t)
|
|
404
|
+
t.Setenv("TYPESAFE_API_KEY", testKey)
|
|
405
|
+
srv := newFakeJev(t, always(clearGate()))
|
|
406
|
+
e.writeGlobal(t, jevConfig(srv, map[string]any{"gate": map[string]any{"enabled": false}}))
|
|
407
|
+
h := start(t, e, newHostState(), HostOptions{})
|
|
408
|
+
h.toolCall("bash", bash("x"))
|
|
409
|
+
if srv.count() != 0 {
|
|
410
|
+
t.Fatal("judged with gate.enabled=false")
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// ---- cache and in-flight sharing --------------------------------------------------
|
|
415
|
+
|
|
416
|
+
func TestGate_IdenticalCallIsJudgedOncePerWindow(t *testing.T) {
|
|
417
|
+
e := newEnv(t)
|
|
418
|
+
t.Setenv("TYPESAFE_API_KEY", testKey)
|
|
419
|
+
srv := newFakeJev(t, always(clearGate()))
|
|
420
|
+
e.writeGlobal(t, jevConfig(srv, nil))
|
|
421
|
+
hs := newHostState()
|
|
422
|
+
hs.user = "list files"
|
|
423
|
+
h := start(t, e, hs, HostOptions{})
|
|
424
|
+
h.toolCall("bash", bash("ls"))
|
|
425
|
+
h.toolCall("bash", bash("ls"))
|
|
426
|
+
if srv.count() != 1 {
|
|
427
|
+
t.Fatalf("requests = %d, want 1", srv.count())
|
|
428
|
+
}
|
|
429
|
+
h.toolCall("bash", bash("ls -la"))
|
|
430
|
+
if srv.count() != 2 {
|
|
431
|
+
t.Fatalf("different input reused a verdict: %d requests", srv.count())
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
func TestGate_CacheKeyIgnoresPropertyOrder(t *testing.T) {
|
|
436
|
+
e := newEnv(t)
|
|
437
|
+
t.Setenv("TYPESAFE_API_KEY", testKey)
|
|
438
|
+
srv := newFakeJev(t, always(clearGate()))
|
|
439
|
+
e.writeGlobal(t, jevConfig(srv, nil))
|
|
440
|
+
h := start(t, e, newHostState(), HostOptions{})
|
|
441
|
+
h.Fire("tool_call", map[string]any{"toolName": "write", "input": json.RawMessage(`{"path":"a","content":"b"}`)})
|
|
442
|
+
h.Fire("tool_call", map[string]any{"toolName": "write", "input": json.RawMessage(`{"content":"b","path":"a"}`)})
|
|
443
|
+
if srv.count() != 1 {
|
|
444
|
+
t.Fatalf("requests = %d, want 1", srv.count())
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// C1: probe from the review. The same arguments after the user changed their
|
|
449
|
+
// request reused a verdict that had assessed the old intent.
|
|
450
|
+
func TestCorrection_CacheIsBoundToTheUserRequest(t *testing.T) {
|
|
451
|
+
e := newEnv(t)
|
|
452
|
+
t.Setenv("TYPESAFE_API_KEY", testKey)
|
|
453
|
+
srv := newFakeJev(t, func(i int, _ recordedRequest) jevReply {
|
|
454
|
+
if i == 0 {
|
|
455
|
+
return jevReply{body: clearGate()}
|
|
456
|
+
}
|
|
457
|
+
return jevReply{body: gateBody(0.1, 0.1, 0.99, 0.5, 0.9)}
|
|
458
|
+
})
|
|
459
|
+
e.writeGlobal(t, jevConfig(srv, nil))
|
|
460
|
+
hs := newHostState()
|
|
461
|
+
hs.user = "delete the build directory"
|
|
462
|
+
h := start(t, e, hs, HostOptions{})
|
|
463
|
+
h.toolCall("bash", bash("rm -rf build"))
|
|
464
|
+
h.pushUser(hs, "just explain what the build directory is")
|
|
465
|
+
h.toolCall("bash", bash("rm -rf build"))
|
|
466
|
+
if srv.count() != 2 {
|
|
467
|
+
t.Fatalf("requests = %d, want 2: the second judgment used a verdict from another request", srv.count())
|
|
468
|
+
}
|
|
469
|
+
if !strings.Contains(h.lastStatus(), "beyond_scope") {
|
|
470
|
+
t.Errorf("status = %q, want the second verdict", h.lastStatus())
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
func TestCorrection_CacheIsBoundToTheModelAndEndpoint(t *testing.T) {
|
|
475
|
+
e := newEnv(t)
|
|
476
|
+
t.Setenv("TYPESAFE_API_KEY", testKey)
|
|
477
|
+
srv := newFakeJev(t, always(clearGate()))
|
|
478
|
+
e.writeGlobal(t, jevConfig(srv, nil))
|
|
479
|
+
h := start(t, e, newHostState(), HostOptions{})
|
|
480
|
+
h.toolCall("bash", bash("ls"))
|
|
481
|
+
e.writeGlobal(t, jevConfig(srv, map[string]any{"model": "jev-other"}))
|
|
482
|
+
h.Fire("session_start", map[string]any{"reason": "reload"})
|
|
483
|
+
h.toolCall("bash", bash("ls"))
|
|
484
|
+
if srv.count() != 2 {
|
|
485
|
+
t.Fatalf("requests = %d: a verdict from another model was reused", srv.count())
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
func TestGate_SiblingCallsShareOneInFlightRequest(t *testing.T) {
|
|
490
|
+
e := newEnv(t)
|
|
491
|
+
t.Setenv("TYPESAFE_API_KEY", testKey)
|
|
492
|
+
srv := newFakeJev(t, func(int, recordedRequest) jevReply {
|
|
493
|
+
return jevReply{body: clearGate(), delay: 300 * time.Millisecond}
|
|
494
|
+
})
|
|
495
|
+
e.writeGlobal(t, jevConfig(srv, nil))
|
|
496
|
+
h := start(t, e, newHostState(), HostOptions{})
|
|
497
|
+
var wg sync.WaitGroup
|
|
498
|
+
for i := 0; i < 3; i++ {
|
|
499
|
+
wg.Add(1)
|
|
500
|
+
go func() { defer wg.Done(); h.toolCall("bash", bash("ls")) }()
|
|
501
|
+
}
|
|
502
|
+
wg.Wait()
|
|
503
|
+
if srv.count() != 1 {
|
|
504
|
+
t.Fatalf("requests = %d, want 1 shared request", srv.count())
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
func TestGate_ErrorsAreNotCached(t *testing.T) {
|
|
509
|
+
e := newEnv(t)
|
|
510
|
+
t.Setenv("TYPESAFE_API_KEY", testKey)
|
|
511
|
+
srv := newFakeJev(t, func(i int, _ recordedRequest) jevReply {
|
|
512
|
+
if i == 0 {
|
|
513
|
+
return jevReply{status: 500, body: "boom"}
|
|
514
|
+
}
|
|
515
|
+
return jevReply{body: clearGate()}
|
|
516
|
+
})
|
|
517
|
+
e.writeGlobal(t, jevConfig(srv, nil))
|
|
518
|
+
h := start(t, e, newHostState(), HostOptions{})
|
|
519
|
+
h.toolCall("bash", bash("ls"))
|
|
520
|
+
h.toolCall("bash", bash("ls"))
|
|
521
|
+
if srv.count() != 2 {
|
|
522
|
+
t.Fatalf("requests = %d, want 2", srv.count())
|
|
523
|
+
}
|
|
524
|
+
if h.lastStatus() != "jev: clear (shadow)" {
|
|
525
|
+
t.Errorf("status = %q", h.lastStatus())
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
func TestGate_UserRequestIsTrimmed(t *testing.T) {
|
|
530
|
+
e := newEnv(t)
|
|
531
|
+
t.Setenv("TYPESAFE_API_KEY", testKey)
|
|
532
|
+
srv := newFakeJev(t, always(clearGate()))
|
|
533
|
+
e.writeGlobal(t, jevConfig(srv, nil))
|
|
534
|
+
hs := newHostState()
|
|
535
|
+
hs.user = " \n list the files \n"
|
|
536
|
+
h := start(t, e, hs, HostOptions{})
|
|
537
|
+
h.toolCall("bash", bash("ls"))
|
|
538
|
+
if got := stateOf(t, srv.first(t))["user_request"]; got != "list the files" {
|
|
539
|
+
t.Errorf("user_request = %q", got)
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
func TestGate_LatestUserMessageWins(t *testing.T) {
|
|
544
|
+
e := newEnv(t)
|
|
545
|
+
t.Setenv("TYPESAFE_API_KEY", testKey)
|
|
546
|
+
srv := newFakeJev(t, always(clearGate()))
|
|
547
|
+
e.writeGlobal(t, jevConfig(srv, nil))
|
|
548
|
+
hs := newHostState()
|
|
549
|
+
hs.user = "first request"
|
|
550
|
+
h := start(t, e, hs, HostOptions{})
|
|
551
|
+
h.toolCall("bash", bash("ls"))
|
|
552
|
+
h.pushUser(hs, "second request")
|
|
553
|
+
h.toolCall("bash", bash("ls -la"))
|
|
554
|
+
reqs := srv.requests()
|
|
555
|
+
if got := stateOf(t, reqs[1])["user_request"]; got != "second request" {
|
|
556
|
+
t.Errorf("user_request = %q", got)
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// The cap shrinks the long strings before it drops anything.
|
|
561
|
+
func TestCorrection_MaxStateCharsKeepsShortenedArguments(t *testing.T) {
|
|
562
|
+
e := newEnv(t)
|
|
563
|
+
t.Setenv("TYPESAFE_API_KEY", testKey)
|
|
564
|
+
srv := newFakeJev(t, always(clearGate()))
|
|
565
|
+
e.writeGlobal(t, jevConfig(srv, map[string]any{"maxStateChars": 500}))
|
|
566
|
+
h := start(t, e, newHostState(), HostOptions{})
|
|
567
|
+
h.toolCall("write", map[string]any{"path": "f.txt", "content": strings.Repeat("c", 900)})
|
|
568
|
+
st := stateOf(t, srv.first(t))
|
|
569
|
+
args, ok := st["arguments"].(map[string]any)
|
|
570
|
+
if !ok || args["path"] != "f.txt" {
|
|
571
|
+
t.Fatalf("arguments dropped although shortening was enough: %v", st["arguments"])
|
|
572
|
+
}
|
|
573
|
+
if got := args["content"].(string); !strings.HasPrefix(got, "cccc") || !strings.Contains(got, "chars elided]") || len(got) > 300 {
|
|
574
|
+
t.Errorf("content = %.60q...", got)
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
func TestCorrection_ProjectCannotRaiseTheStateCapOrOutputLimit(t *testing.T) {
|
|
579
|
+
e := newEnv(t)
|
|
580
|
+
t.Setenv("TYPESAFE_API_KEY", testKey)
|
|
581
|
+
srv := newFakeJev(t, always(clearGate()))
|
|
582
|
+
e.writeGlobal(t, jevConfig(srv, nil))
|
|
583
|
+
e.writeProject(t, map[string]any{"maxStateChars": 999999, "output": map[string]any{"outputChars": 999999}})
|
|
584
|
+
h := start(t, e, newHostState(), HostOptions{})
|
|
585
|
+
in := map[string]any{}
|
|
586
|
+
for i := 0; i < 60; i++ {
|
|
587
|
+
in["field"+strconv.Itoa(i)] = strings.Repeat("x", 300)
|
|
588
|
+
}
|
|
589
|
+
h.toolCall("bash", in)
|
|
590
|
+
if n := utf16Units(srv.first(t).Raw); n > 8000+4000 { // the state is capped at 8000; the questions add a fixed amount
|
|
591
|
+
t.Errorf("request is %d units: the project raised maxStateChars", n)
|
|
592
|
+
}
|
|
593
|
+
h.toolResult("bash", bash("y"), text(strings.Repeat("o", 5000)), false)
|
|
594
|
+
reqs := srv.requests()
|
|
595
|
+
out := stateOf(t, reqs[len(reqs)-1])["output"].(string)
|
|
596
|
+
if !strings.HasSuffix(out, "[3000 chars elided]") {
|
|
597
|
+
t.Errorf("project raised output.outputChars: ...%s", out[len(out)-30:])
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
func utf16Units(s string) int {
|
|
602
|
+
n := 0
|
|
603
|
+
for _, r := range s {
|
|
604
|
+
n++
|
|
605
|
+
if r >= 0x10000 {
|
|
606
|
+
n++
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
return n
|
|
610
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
package jev
|
|
2
|
+
|
|
3
|
+
import "testing"
|
|
4
|
+
|
|
5
|
+
func TestGateKeyIsBoundedAndDistinguishesCalls(t *testing.T) {
|
|
6
|
+
big := benchInput(200_000)
|
|
7
|
+
k := gateKey("write", big, "/work", "do it")
|
|
8
|
+
if len(k) > 128 {
|
|
9
|
+
t.Fatalf("the memo key is %d bytes for a 200 KB call; it must be a digest, not the arguments", len(k))
|
|
10
|
+
}
|
|
11
|
+
if k != gateKey("write", benchInput(200_000), "/work", "do it") {
|
|
12
|
+
t.Fatal("the same call must produce the same key")
|
|
13
|
+
}
|
|
14
|
+
for name, other := range map[string]string{
|
|
15
|
+
"tool": gateKey("edit", big, "/work", "do it"),
|
|
16
|
+
"cwd": gateKey("write", big, "/other", "do it"),
|
|
17
|
+
"user": gateKey("write", big, "/work", "do something else"),
|
|
18
|
+
"args": gateKey("write", benchInput(200_042), "/work", "do it"),
|
|
19
|
+
} {
|
|
20
|
+
if other == k {
|
|
21
|
+
t.Errorf("a different %s must produce a different key", name)
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
module github.com/MichaelKinsy/pigpen/jev
|
|
2
|
+
|
|
3
|
+
go 1.26
|
|
4
|
+
|
|
5
|
+
// PiG resolves the SDK requirement to the version-matched staged SDK at build time.
|
|
6
|
+
require github.com/MichaelKinsy/PiG/extensions/sdk v0.4.1
|
|
7
|
+
|
|
8
|
+
// The shared TypeSafe client (components/typesafe), found through go.work in this directory.
|
|
9
|
+
require github.com/MichaelKinsy/pigpen/components/typesafe v0.0.0
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
go 1.26
|
|
2
|
+
|
|
3
|
+
// PiG's extension resolver reads this file (extension/source: goWorkspaceModuleRoots) to
|
|
4
|
+
// find the shared library module, and adds the require and replace itself when it fuses
|
|
5
|
+
// or builds. The relative path is the same in the repository and in a staged Piglet
|
|
6
|
+
// (packages/<package>/extensions/<name> -> packages/typesafe).
|
|
7
|
+
use (
|
|
8
|
+
.
|
|
9
|
+
../../libs/typesafe
|
|
10
|
+
)
|