@dannylee1020/kkt 0.5.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/LICENSE +201 -0
- package/README.md +275 -0
- package/assets/kkt-readme-modern.png +0 -0
- package/bin/kkt-install.mjs +25 -0
- package/cmd/kkt/main.go +15 -0
- package/go.mod +3 -0
- package/internal/workflow/cli.go +349 -0
- package/internal/workflow/cli_test.go +1266 -0
- package/internal/workflow/guardrails.go +900 -0
- package/internal/workflow/init.go +164 -0
- package/internal/workflow/init_test.go +113 -0
- package/internal/workflow/operations.go +1855 -0
- package/internal/workflow/validation_commands.go +291 -0
- package/internal/workflow/workspace.go +802 -0
- package/internal/workflow/workspace_test.go +285 -0
- package/package.json +45 -0
- package/scripts/install-cli.sh +210 -0
- package/scripts/install.sh +644 -0
- package/skills/kkt/SKILL.md +74 -0
- package/skills/kkt/references/discovery-tooling.md +53 -0
- package/skills/kkt/references/feature-optimization-model.md +120 -0
- package/skills/kkt/references/kkt-kernel.md +58 -0
- package/skills/kkt/references/layered-modeling-methods.md +101 -0
- package/skills/kkt/references/plan-assimilation.md +46 -0
- package/skills/kkt/references/schemas.md +231 -0
- package/skills/kkt/references/state-contract.md +76 -0
- package/skills/kkt-loop/SKILL.md +99 -0
- package/skills/kkt-loop/references/discovery-tooling.md +53 -0
- package/skills/kkt-loop/references/feature-optimization-model.md +120 -0
- package/skills/kkt-loop/references/kkt-kernel.md +58 -0
- package/skills/kkt-loop/references/layered-modeling-methods.md +101 -0
- package/skills/kkt-loop/references/plan-assimilation.md +46 -0
- package/skills/kkt-loop/references/schemas.md +231 -0
- package/skills/kkt-loop/references/state-contract.md +76 -0
- package/skills/kkt-model/SKILL.md +76 -0
- package/skills/kkt-model/references/discovery-tooling.md +53 -0
- package/skills/kkt-model/references/feature-optimization-model.md +120 -0
- package/skills/kkt-model/references/kkt-kernel.md +58 -0
- package/skills/kkt-model/references/layered-modeling-methods.md +101 -0
- package/skills/kkt-model/references/plan-assimilation.md +46 -0
- package/skills/kkt-model/references/schemas.md +231 -0
- package/skills/kkt-model/references/state-contract.md +76 -0
- package/skills/kkt-run/SKILL.md +62 -0
- package/skills/kkt-run/references/discovery-tooling.md +53 -0
- package/skills/kkt-run/references/feature-optimization-model.md +120 -0
- package/skills/kkt-run/references/kkt-kernel.md +58 -0
- package/skills/kkt-run/references/layered-modeling-methods.md +101 -0
- package/skills/kkt-run/references/plan-assimilation.md +46 -0
- package/skills/kkt-run/references/schemas.md +231 -0
- package/skills/kkt-run/references/state-contract.md +76 -0
|
@@ -0,0 +1,1266 @@
|
|
|
1
|
+
package workflow
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"bytes"
|
|
5
|
+
"encoding/json"
|
|
6
|
+
"os"
|
|
7
|
+
"os/exec"
|
|
8
|
+
"path/filepath"
|
|
9
|
+
"strings"
|
|
10
|
+
"testing"
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
func TestRunPrintsVersion(t *testing.T) {
|
|
14
|
+
previous := Version
|
|
15
|
+
Version = "vtest"
|
|
16
|
+
defer func() {
|
|
17
|
+
Version = previous
|
|
18
|
+
}()
|
|
19
|
+
|
|
20
|
+
var stdout bytes.Buffer
|
|
21
|
+
if err := Run([]string{"--version"}, &stdout, &bytes.Buffer{}); err != nil {
|
|
22
|
+
t.Fatal(err)
|
|
23
|
+
}
|
|
24
|
+
if got, want := strings.TrimSpace(stdout.String()), "kkt vtest"; got != want {
|
|
25
|
+
t.Fatalf("version output = %q, want %q", got, want)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
func TestRunRejectsRemovedAliasesAndFlags(t *testing.T) {
|
|
30
|
+
tests := [][]string{
|
|
31
|
+
{"-h"},
|
|
32
|
+
{"-v"},
|
|
33
|
+
{"version"},
|
|
34
|
+
{"classify", "implement a feature"},
|
|
35
|
+
{"start", "--profile", "plan", "implement a feature"},
|
|
36
|
+
{"init", "codex"},
|
|
37
|
+
{"uninstall", "--dry-run"},
|
|
38
|
+
{"uninstall", "--keep-binary"},
|
|
39
|
+
{"intent", "--method"},
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
for _, test := range tests {
|
|
43
|
+
t.Run(strings.Join(test, " "), func(t *testing.T) {
|
|
44
|
+
if err := Run(test, &bytes.Buffer{}, &bytes.Buffer{}); err == nil {
|
|
45
|
+
t.Fatal("expected removed alias or flag to be rejected")
|
|
46
|
+
}
|
|
47
|
+
})
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
func TestRunArtifactRecordsLayerMethodAndAdvances(t *testing.T) {
|
|
52
|
+
root := t.TempDir()
|
|
53
|
+
previous, err := os.Getwd()
|
|
54
|
+
if err != nil {
|
|
55
|
+
t.Fatal(err)
|
|
56
|
+
}
|
|
57
|
+
defer func() {
|
|
58
|
+
if err := os.Chdir(previous); err != nil {
|
|
59
|
+
t.Fatal(err)
|
|
60
|
+
}
|
|
61
|
+
}()
|
|
62
|
+
if err := os.Chdir(root); err != nil {
|
|
63
|
+
t.Fatal(err)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
commands := [][]string{
|
|
67
|
+
{"start", "model", "choose", "API", "shape"},
|
|
68
|
+
{"intent", "--method", "why_how", "Clarified owner tradeoffs"},
|
|
69
|
+
{"discovery", "--method", "coupling_map", "Mapped affected API callers"},
|
|
70
|
+
{"model", "--method", "ordinal_mcda", "Compared feasible API shapes"},
|
|
71
|
+
}
|
|
72
|
+
for _, command := range commands {
|
|
73
|
+
if err := Run(command, &bytes.Buffer{}, &bytes.Buffer{}); err != nil {
|
|
74
|
+
t.Fatalf("Run(%v) error = %v", command, err)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
workspace, err := ResolveWorkspace(".", "")
|
|
79
|
+
if err != nil {
|
|
80
|
+
t.Fatal(err)
|
|
81
|
+
}
|
|
82
|
+
state, err := os.ReadFile(filepath.Join(workspace, "kkt.yaml"))
|
|
83
|
+
if err != nil {
|
|
84
|
+
t.Fatal(err)
|
|
85
|
+
}
|
|
86
|
+
text := string(state)
|
|
87
|
+
for _, want := range []string{
|
|
88
|
+
`active_layer: "validation"`,
|
|
89
|
+
`method: "why_how"`,
|
|
90
|
+
`method: "coupling_map"`,
|
|
91
|
+
`method: "ordinal_mcda"`,
|
|
92
|
+
"method_invocations:",
|
|
93
|
+
} {
|
|
94
|
+
if !strings.Contains(text, want) {
|
|
95
|
+
t.Fatalf("state missing %q:\n%s", want, text)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
var next bytes.Buffer
|
|
100
|
+
if err := Run([]string{"next"}, &next, &bytes.Buffer{}); err != nil {
|
|
101
|
+
t.Fatal(err)
|
|
102
|
+
}
|
|
103
|
+
if text := next.String(); !strings.Contains(text, "run kkt validate") || strings.Contains(text, "kkt evidence") {
|
|
104
|
+
t.Fatalf("model-only next should validate without evidence guidance:\n%s", text)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
func TestRunArtifactRejectsInvalidLayerMethod(t *testing.T) {
|
|
109
|
+
root := t.TempDir()
|
|
110
|
+
previous, err := os.Getwd()
|
|
111
|
+
if err != nil {
|
|
112
|
+
t.Fatal(err)
|
|
113
|
+
}
|
|
114
|
+
defer func() {
|
|
115
|
+
if err := os.Chdir(previous); err != nil {
|
|
116
|
+
t.Fatal(err)
|
|
117
|
+
}
|
|
118
|
+
}()
|
|
119
|
+
if err := os.Chdir(root); err != nil {
|
|
120
|
+
t.Fatal(err)
|
|
121
|
+
}
|
|
122
|
+
if err := Run([]string{"start", "model", "choose", "API", "shape"}, &bytes.Buffer{}, &bytes.Buffer{}); err != nil {
|
|
123
|
+
t.Fatal(err)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
err = Run([]string{"model", "--method", "goal_anti_goal", "wrong layer method"}, &bytes.Buffer{}, &bytes.Buffer{})
|
|
127
|
+
if err == nil {
|
|
128
|
+
t.Fatal("expected invalid model method to fail")
|
|
129
|
+
}
|
|
130
|
+
if !strings.Contains(err.Error(), "unsupported model method") {
|
|
131
|
+
t.Fatalf("error = %q, want unsupported model method", err.Error())
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
func TestRunGuardrailsShowAndValidate(t *testing.T) {
|
|
136
|
+
root := t.TempDir()
|
|
137
|
+
previous, err := os.Getwd()
|
|
138
|
+
if err != nil {
|
|
139
|
+
t.Fatal(err)
|
|
140
|
+
}
|
|
141
|
+
defer func() {
|
|
142
|
+
if err := os.Chdir(previous); err != nil {
|
|
143
|
+
t.Fatal(err)
|
|
144
|
+
}
|
|
145
|
+
}()
|
|
146
|
+
if err := os.Chdir(root); err != nil {
|
|
147
|
+
t.Fatal(err)
|
|
148
|
+
}
|
|
149
|
+
if err := Run([]string{"start", "model", "choose", "API", "shape"}, &bytes.Buffer{}, &bytes.Buffer{}); err != nil {
|
|
150
|
+
t.Fatal(err)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
var show bytes.Buffer
|
|
154
|
+
if err := Run([]string{"guardrails", "show"}, &show, &bytes.Buffer{}); err != nil {
|
|
155
|
+
t.Fatal(err)
|
|
156
|
+
}
|
|
157
|
+
if text := show.String(); !strings.Contains(text, `"schema_version": 1`) || !strings.Contains(text, `"drift_policy"`) {
|
|
158
|
+
t.Fatalf("guardrails show missing contract fields:\n%s", text)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
var validate bytes.Buffer
|
|
162
|
+
if err := Run([]string{"guardrails", "validate"}, &validate, &bytes.Buffer{}); err != nil {
|
|
163
|
+
t.Fatal(err)
|
|
164
|
+
}
|
|
165
|
+
if text := validate.String(); !strings.Contains(text, "valid:") {
|
|
166
|
+
t.Fatalf("guardrails validate did not pass:\n%s", text)
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
func TestRunGuardrailsValidateRequiresSourceWorkspace(t *testing.T) {
|
|
171
|
+
root := t.TempDir()
|
|
172
|
+
previous, err := os.Getwd()
|
|
173
|
+
if err != nil {
|
|
174
|
+
t.Fatal(err)
|
|
175
|
+
}
|
|
176
|
+
defer func() {
|
|
177
|
+
if err := os.Chdir(previous); err != nil {
|
|
178
|
+
t.Fatal(err)
|
|
179
|
+
}
|
|
180
|
+
}()
|
|
181
|
+
if err := os.Chdir(root); err != nil {
|
|
182
|
+
t.Fatal(err)
|
|
183
|
+
}
|
|
184
|
+
if err := Run([]string{"start", "model", "choose", "API", "shape"}, &bytes.Buffer{}, &bytes.Buffer{}); err != nil {
|
|
185
|
+
t.Fatal(err)
|
|
186
|
+
}
|
|
187
|
+
invalid := strings.Replace(testGuardrailsJSON("model", []string{"**"}, nil), `"workspace":".kkt/test-workspace"`, `"workspace":""`, 1)
|
|
188
|
+
if err := Run([]string{"guardrails", "set", invalid}, &bytes.Buffer{}, &bytes.Buffer{}); err != nil {
|
|
189
|
+
t.Fatal(err)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
var validate bytes.Buffer
|
|
193
|
+
err = Run([]string{"guardrails", "validate"}, &validate, &bytes.Buffer{})
|
|
194
|
+
if err == nil {
|
|
195
|
+
t.Fatal("guardrails validate should fail when source.workspace is empty")
|
|
196
|
+
}
|
|
197
|
+
if text := validate.String(); !strings.Contains(text, "source.workspace is required") {
|
|
198
|
+
t.Fatalf("guardrails validation output missing source workspace issue:\n%s", text)
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
func TestRunFromModelCreatesApprovalGatedRun(t *testing.T) {
|
|
203
|
+
root := t.TempDir()
|
|
204
|
+
previous, err := os.Getwd()
|
|
205
|
+
if err != nil {
|
|
206
|
+
t.Fatal(err)
|
|
207
|
+
}
|
|
208
|
+
defer func() {
|
|
209
|
+
if err := os.Chdir(previous); err != nil {
|
|
210
|
+
t.Fatal(err)
|
|
211
|
+
}
|
|
212
|
+
}()
|
|
213
|
+
if err := os.Chdir(root); err != nil {
|
|
214
|
+
t.Fatal(err)
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
commands := [][]string{
|
|
218
|
+
{"start", "model", "choose", "API", "shape"},
|
|
219
|
+
{"intent", "--method", "why_how", "Clarified owner tradeoffs"},
|
|
220
|
+
{"discovery", "--method", "coupling_map", "Mapped affected API callers"},
|
|
221
|
+
{"model", "--method", "ordinal_mcda", "Compared feasible API shapes"},
|
|
222
|
+
{"guardrails", "set", testGuardrailsJSON("model", []string{"internal/workflow/**"}, nil)},
|
|
223
|
+
{"done", "Model complete"},
|
|
224
|
+
}
|
|
225
|
+
for _, command := range commands {
|
|
226
|
+
if err := Run(command, &bytes.Buffer{}, &bytes.Buffer{}); err != nil {
|
|
227
|
+
t.Fatalf("Run(%v) error = %v", command, err)
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
var created bytes.Buffer
|
|
232
|
+
if err := Run([]string{"run", "from-model"}, &created, &bytes.Buffer{}); err != nil {
|
|
233
|
+
t.Fatal(err)
|
|
234
|
+
}
|
|
235
|
+
if text := created.String(); !strings.Contains(text, "profile: run") {
|
|
236
|
+
t.Fatalf("run from-model output missing run profile:\n%s", text)
|
|
237
|
+
}
|
|
238
|
+
workspace, err := ResolveWorkspace(".", "")
|
|
239
|
+
if err != nil {
|
|
240
|
+
t.Fatal(err)
|
|
241
|
+
}
|
|
242
|
+
if filepath.Base(filepath.Dir(workspace)) != "run" {
|
|
243
|
+
t.Fatalf("current workspace = %q, want run workspace", workspace)
|
|
244
|
+
}
|
|
245
|
+
stateBytes, err := os.ReadFile(filepath.Join(workspace, "kkt.yaml"))
|
|
246
|
+
if err != nil {
|
|
247
|
+
t.Fatal(err)
|
|
248
|
+
}
|
|
249
|
+
stateText := string(stateBytes)
|
|
250
|
+
for _, want := range []string{
|
|
251
|
+
`intent:`,
|
|
252
|
+
`status: "complete"`,
|
|
253
|
+
`method: "imported"`,
|
|
254
|
+
`method_invocations:`,
|
|
255
|
+
} {
|
|
256
|
+
if !strings.Contains(stateText, want) {
|
|
257
|
+
t.Fatalf("run state missing imported model marker %q:\n%s", want, stateText)
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
var judge bytes.Buffer
|
|
262
|
+
if err := Run([]string{"judge", "--checkpoint", "model-ready", "--json"}, &judge, &bytes.Buffer{}); err != nil {
|
|
263
|
+
t.Fatal(err)
|
|
264
|
+
}
|
|
265
|
+
if text := judge.String(); !strings.Contains(text, `"verdict": "allow"`) || !strings.Contains(text, `"workspace_type": "run"`) {
|
|
266
|
+
t.Fatalf("model-ready judge should allow complete run contract:\n%s", text)
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
var next bytes.Buffer
|
|
270
|
+
if err := Run([]string{"next", "--json"}, &next, &bytes.Buffer{}); err != nil {
|
|
271
|
+
t.Fatal(err)
|
|
272
|
+
}
|
|
273
|
+
if text := next.String(); !strings.Contains(text, `"action": "request_approval"`) || !strings.Contains(text, `"blocked": true`) {
|
|
274
|
+
t.Fatalf("run next should request approval before mutation:\n%s", text)
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
var blocked bytes.Buffer
|
|
278
|
+
err = Run([]string{"judge", "--checkpoint", "pre-mutation", "--json"}, &blocked, &bytes.Buffer{})
|
|
279
|
+
if err == nil {
|
|
280
|
+
t.Fatal("pre-mutation judge should block without approval")
|
|
281
|
+
}
|
|
282
|
+
if text := blocked.String(); !strings.Contains(text, `"verdict": "block"`) || !strings.Contains(text, `"drift_type": "approval"`) {
|
|
283
|
+
t.Fatalf("pre-mutation judge output missing approval block:\n%s", text)
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
func TestRunJudgeAllowsUnrelatedChangedPathOutsideAllowedBounds(t *testing.T) {
|
|
288
|
+
root := t.TempDir()
|
|
289
|
+
previous, err := os.Getwd()
|
|
290
|
+
if err != nil {
|
|
291
|
+
t.Fatal(err)
|
|
292
|
+
}
|
|
293
|
+
defer func() {
|
|
294
|
+
if err := os.Chdir(previous); err != nil {
|
|
295
|
+
t.Fatal(err)
|
|
296
|
+
}
|
|
297
|
+
}()
|
|
298
|
+
if err := os.Chdir(root); err != nil {
|
|
299
|
+
t.Fatal(err)
|
|
300
|
+
}
|
|
301
|
+
initGit(t, root)
|
|
302
|
+
|
|
303
|
+
commands := [][]string{
|
|
304
|
+
{"start", "run", "path", "scope"},
|
|
305
|
+
{"intent", "--method", "goal_anti_goal", "Captured path scope"},
|
|
306
|
+
{"discovery", "--method", "traceability_matrix", "Expected workflow-only change"},
|
|
307
|
+
{"model", "--method", "lexicographic", "Selected workflow-only plan"},
|
|
308
|
+
{"guardrails", "set", testGuardrailsJSON("run", []string{"internal/workflow/**"}, nil)},
|
|
309
|
+
{"approve", "Approved workflow-only scope"},
|
|
310
|
+
}
|
|
311
|
+
for _, command := range commands {
|
|
312
|
+
if err := Run(command, &bytes.Buffer{}, &bytes.Buffer{}); err != nil {
|
|
313
|
+
t.Fatalf("Run(%v) error = %v", command, err)
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
if err := os.WriteFile(filepath.Join(root, "README.md"), []byte("out of scope\n"), 0o644); err != nil {
|
|
317
|
+
t.Fatal(err)
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
var allowed bytes.Buffer
|
|
321
|
+
if err := Run([]string{"judge", "--checkpoint", "pre-mutation", "--json"}, &allowed, &bytes.Buffer{}); err != nil {
|
|
322
|
+
t.Fatalf("unrelated path outside allowed bounds should not block:\n%s", allowed.String())
|
|
323
|
+
}
|
|
324
|
+
if text := allowed.String(); !strings.Contains(text, `"verdict": "allow"`) || strings.Contains(text, "changed path outside allowed bounds: README.md") {
|
|
325
|
+
t.Fatalf("pre-mutation judge should ignore unrelated out-of-scope path:\n%s", text)
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
func TestRunJudgeAllowsChangedPathInsideAllowedBounds(t *testing.T) {
|
|
330
|
+
root := t.TempDir()
|
|
331
|
+
previous, err := os.Getwd()
|
|
332
|
+
if err != nil {
|
|
333
|
+
t.Fatal(err)
|
|
334
|
+
}
|
|
335
|
+
defer func() {
|
|
336
|
+
if err := os.Chdir(previous); err != nil {
|
|
337
|
+
t.Fatal(err)
|
|
338
|
+
}
|
|
339
|
+
}()
|
|
340
|
+
if err := os.Chdir(root); err != nil {
|
|
341
|
+
t.Fatal(err)
|
|
342
|
+
}
|
|
343
|
+
initGit(t, root)
|
|
344
|
+
|
|
345
|
+
commands := [][]string{
|
|
346
|
+
{"start", "run", "path", "scope"},
|
|
347
|
+
{"intent", "--method", "goal_anti_goal", "Captured path scope"},
|
|
348
|
+
{"discovery", "--method", "traceability_matrix", "Expected workflow-only change"},
|
|
349
|
+
{"model", "--method", "lexicographic", "Selected workflow-only plan"},
|
|
350
|
+
{"guardrails", "set", testGuardrailsJSON("run", []string{"internal/workflow/**"}, nil)},
|
|
351
|
+
{"approve", "Approved workflow-only scope"},
|
|
352
|
+
}
|
|
353
|
+
for _, command := range commands {
|
|
354
|
+
if err := Run(command, &bytes.Buffer{}, &bytes.Buffer{}); err != nil {
|
|
355
|
+
t.Fatalf("Run(%v) error = %v", command, err)
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
if err := os.MkdirAll(filepath.Join(root, "internal", "workflow"), 0o755); err != nil {
|
|
359
|
+
t.Fatal(err)
|
|
360
|
+
}
|
|
361
|
+
if err := os.WriteFile(filepath.Join(root, "internal", "workflow", "change.go"), []byte("package workflow\n"), 0o644); err != nil {
|
|
362
|
+
t.Fatal(err)
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
var allowed bytes.Buffer
|
|
366
|
+
if err := Run([]string{"judge", "--checkpoint", "pre-mutation", "--json"}, &allowed, &bytes.Buffer{}); err != nil {
|
|
367
|
+
t.Fatal(err)
|
|
368
|
+
}
|
|
369
|
+
if text := allowed.String(); !strings.Contains(text, `"verdict": "allow"`) {
|
|
370
|
+
t.Fatalf("pre-mutation judge should allow in-scope changes:\n%s", text)
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
func TestApprovalBaselineAndUnrelatedDirtyPathDoNotBlock(t *testing.T) {
|
|
375
|
+
root := t.TempDir()
|
|
376
|
+
previous, err := os.Getwd()
|
|
377
|
+
if err != nil {
|
|
378
|
+
t.Fatal(err)
|
|
379
|
+
}
|
|
380
|
+
defer func() {
|
|
381
|
+
if err := os.Chdir(previous); err != nil {
|
|
382
|
+
t.Fatal(err)
|
|
383
|
+
}
|
|
384
|
+
}()
|
|
385
|
+
if err := os.Chdir(root); err != nil {
|
|
386
|
+
t.Fatal(err)
|
|
387
|
+
}
|
|
388
|
+
initGit(t, root)
|
|
389
|
+
if err := os.WriteFile(filepath.Join(root, "README.md"), []byte("preexisting\n"), 0o644); err != nil {
|
|
390
|
+
t.Fatal(err)
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
commands := [][]string{
|
|
394
|
+
{"start", "run", "path", "scope"},
|
|
395
|
+
{"intent", "--method", "goal_anti_goal", "Captured path scope"},
|
|
396
|
+
{"discovery", "--method", "traceability_matrix", "Expected workflow-only change"},
|
|
397
|
+
{"model", "--method", "lexicographic", "Selected workflow-only plan"},
|
|
398
|
+
{"guardrails", "set", testGuardrailsJSON("run", []string{"internal/workflow/**"}, nil)},
|
|
399
|
+
{"approve", "Approved workflow-only scope"},
|
|
400
|
+
}
|
|
401
|
+
for _, command := range commands {
|
|
402
|
+
if err := Run(command, &bytes.Buffer{}, &bytes.Buffer{}); err != nil {
|
|
403
|
+
t.Fatalf("Run(%v) error = %v", command, err)
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
var allowed bytes.Buffer
|
|
408
|
+
if err := Run([]string{"judge", "--checkpoint", "pre-mutation", "--json"}, &allowed, &bytes.Buffer{}); err != nil {
|
|
409
|
+
t.Fatalf("unchanged preexisting dirty path should not block:\n%s", allowed.String())
|
|
410
|
+
}
|
|
411
|
+
if text := allowed.String(); !strings.Contains(text, `"verdict": "allow"`) {
|
|
412
|
+
t.Fatalf("pre-mutation judge should allow unchanged preexisting dirty path:\n%s", text)
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
if err := os.WriteFile(filepath.Join(root, "README.md"), []byte("changed after approval\n"), 0o644); err != nil {
|
|
416
|
+
t.Fatal(err)
|
|
417
|
+
}
|
|
418
|
+
var stillAllowed bytes.Buffer
|
|
419
|
+
if err := Run([]string{"judge", "--checkpoint", "pre-mutation", "--json"}, &stillAllowed, &bytes.Buffer{}); err != nil {
|
|
420
|
+
t.Fatalf("changed unrelated out-of-scope path should not block:\n%s", stillAllowed.String())
|
|
421
|
+
}
|
|
422
|
+
if text := stillAllowed.String(); !strings.Contains(text, `"verdict": "allow"`) || strings.Contains(text, "changed path outside allowed bounds: README.md") {
|
|
423
|
+
t.Fatalf("pre-mutation judge should ignore changed unrelated out-of-scope path:\n%s", text)
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
func TestRunJudgeBlockedPathOverridesAllowedBounds(t *testing.T) {
|
|
428
|
+
root := t.TempDir()
|
|
429
|
+
previous, err := os.Getwd()
|
|
430
|
+
if err != nil {
|
|
431
|
+
t.Fatal(err)
|
|
432
|
+
}
|
|
433
|
+
defer func() {
|
|
434
|
+
if err := os.Chdir(previous); err != nil {
|
|
435
|
+
t.Fatal(err)
|
|
436
|
+
}
|
|
437
|
+
}()
|
|
438
|
+
if err := os.Chdir(root); err != nil {
|
|
439
|
+
t.Fatal(err)
|
|
440
|
+
}
|
|
441
|
+
initGit(t, root)
|
|
442
|
+
|
|
443
|
+
commands := [][]string{
|
|
444
|
+
{"start", "run", "path", "scope"},
|
|
445
|
+
{"intent", "--method", "goal_anti_goal", "Captured path scope"},
|
|
446
|
+
{"discovery", "--method", "traceability_matrix", "Expected broad change"},
|
|
447
|
+
{"model", "--method", "lexicographic", "Selected broad plan with README blocked"},
|
|
448
|
+
{"guardrails", "set", testGuardrailsJSON("run", []string{"**"}, []string{"README.md"})},
|
|
449
|
+
{"approve", "Approved broad scope"},
|
|
450
|
+
}
|
|
451
|
+
for _, command := range commands {
|
|
452
|
+
if err := Run(command, &bytes.Buffer{}, &bytes.Buffer{}); err != nil {
|
|
453
|
+
t.Fatalf("Run(%v) error = %v", command, err)
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
if err := os.WriteFile(filepath.Join(root, "README.md"), []byte("blocked\n"), 0o644); err != nil {
|
|
457
|
+
t.Fatal(err)
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
var blocked bytes.Buffer
|
|
461
|
+
err = Run([]string{"judge", "--checkpoint", "pre-mutation", "--json"}, &blocked, &bytes.Buffer{})
|
|
462
|
+
if err == nil {
|
|
463
|
+
t.Fatal("pre-mutation judge should block explicitly blocked paths")
|
|
464
|
+
}
|
|
465
|
+
if text := blocked.String(); !strings.Contains(text, `"drift_type": "path_scope"`) || !strings.Contains(text, "changed blocked path: README.md") {
|
|
466
|
+
t.Fatalf("pre-mutation judge output missing blocked-path evidence:\n%s", text)
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
func TestRunStartRequiresExplicitProfile(t *testing.T) {
|
|
471
|
+
var stdout bytes.Buffer
|
|
472
|
+
err := Run([]string{"start", "implement", "a", "feature"}, &stdout, &bytes.Buffer{})
|
|
473
|
+
if err == nil {
|
|
474
|
+
t.Fatal("expected start without explicit profile to fail")
|
|
475
|
+
}
|
|
476
|
+
if !strings.Contains(err.Error(), "unsupported profile") {
|
|
477
|
+
t.Fatalf("error = %q, want unsupported profile", err.Error())
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
func TestRunRejectsDailyProfile(t *testing.T) {
|
|
482
|
+
err := Run([]string{"start", "daily", "implement", "a", "feature"}, &bytes.Buffer{}, &bytes.Buffer{})
|
|
483
|
+
if err == nil {
|
|
484
|
+
t.Fatal("expected daily profile to be rejected")
|
|
485
|
+
}
|
|
486
|
+
if !strings.Contains(err.Error(), "unsupported profile") {
|
|
487
|
+
t.Fatalf("error = %q, want unsupported profile", err.Error())
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
func TestPlanArtifactCommandsStayInStateFile(t *testing.T) {
|
|
492
|
+
root := t.TempDir()
|
|
493
|
+
previous, err := os.Getwd()
|
|
494
|
+
if err != nil {
|
|
495
|
+
t.Fatal(err)
|
|
496
|
+
}
|
|
497
|
+
defer func() {
|
|
498
|
+
if err := os.Chdir(previous); err != nil {
|
|
499
|
+
t.Fatal(err)
|
|
500
|
+
}
|
|
501
|
+
}()
|
|
502
|
+
if err := os.Chdir(root); err != nil {
|
|
503
|
+
t.Fatal(err)
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
commands := [][]string{
|
|
507
|
+
{"start", "plan", "make", "the", "CLI", "durable"},
|
|
508
|
+
{"model", "objective_function: keep plan state compact; files_to_modify: workflow state only; constraint_functions: preserve lean kkt tier; decision_variables: typed inline contract; validation_proof: go test ./..."},
|
|
509
|
+
{"evidence", "validated by inspection"},
|
|
510
|
+
}
|
|
511
|
+
for _, command := range commands {
|
|
512
|
+
if err := Run(command, &bytes.Buffer{}, &bytes.Buffer{}); err != nil {
|
|
513
|
+
t.Fatalf("Run(%v) error = %v", command, err)
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
workspace, err := ResolveWorkspace(".", "")
|
|
518
|
+
if err != nil {
|
|
519
|
+
t.Fatal(err)
|
|
520
|
+
}
|
|
521
|
+
if _, err := os.Stat(filepath.Join(workspace, "model.md")); !os.IsNotExist(err) {
|
|
522
|
+
t.Fatalf("plan workspace should not create model.md: %v", err)
|
|
523
|
+
}
|
|
524
|
+
state, err := os.ReadFile(filepath.Join(workspace, "kkt.yaml"))
|
|
525
|
+
if err != nil {
|
|
526
|
+
t.Fatal(err)
|
|
527
|
+
}
|
|
528
|
+
if text := string(state); !strings.Contains(text, "artifact: \"model\"") || !strings.Contains(text, "artifact: \"evidence\"") {
|
|
529
|
+
t.Fatalf("plan artifacts were not recorded in kkt.yaml:\n%s", text)
|
|
530
|
+
}
|
|
531
|
+
for _, want := range []string{
|
|
532
|
+
"planning_contract:",
|
|
533
|
+
"objective_function:",
|
|
534
|
+
"files_to_modify:",
|
|
535
|
+
"constraint_functions:",
|
|
536
|
+
"decision_variables:",
|
|
537
|
+
"validation_proof:",
|
|
538
|
+
} {
|
|
539
|
+
if !strings.Contains(string(state), want) {
|
|
540
|
+
t.Fatalf("plan state missing %q:\n%s", want, state)
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
var stdout bytes.Buffer
|
|
544
|
+
if err := Run([]string{"show", "model"}, &stdout, &bytes.Buffer{}); err != nil {
|
|
545
|
+
t.Fatalf("show model failed for plan workspace: %v", err)
|
|
546
|
+
}
|
|
547
|
+
if !strings.Contains(stdout.String(), "decision_log:") {
|
|
548
|
+
t.Fatalf("show model did not return plan state:\n%s", stdout.String())
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
func TestPlanModelRequiresPlanningContractFields(t *testing.T) {
|
|
553
|
+
root := t.TempDir()
|
|
554
|
+
previous, err := os.Getwd()
|
|
555
|
+
if err != nil {
|
|
556
|
+
t.Fatal(err)
|
|
557
|
+
}
|
|
558
|
+
defer func() {
|
|
559
|
+
if err := os.Chdir(previous); err != nil {
|
|
560
|
+
t.Fatal(err)
|
|
561
|
+
}
|
|
562
|
+
}()
|
|
563
|
+
if err := os.Chdir(root); err != nil {
|
|
564
|
+
t.Fatal(err)
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
if err := Run([]string{"start", "plan", "make", "planning", "explicit"}, &bytes.Buffer{}, &bytes.Buffer{}); err != nil {
|
|
568
|
+
t.Fatal(err)
|
|
569
|
+
}
|
|
570
|
+
err = Run([]string{"model", "selected plan only"}, &bytes.Buffer{}, &bytes.Buffer{})
|
|
571
|
+
if err == nil {
|
|
572
|
+
t.Fatal("expected incomplete plan model to fail")
|
|
573
|
+
}
|
|
574
|
+
if !strings.Contains(err.Error(), "objective_function") || !strings.Contains(err.Error(), "validation_proof") {
|
|
575
|
+
t.Fatalf("error did not list missing planning fields: %v", err)
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
func TestRunStartFromNestedDirectoryUsesProjectRoot(t *testing.T) {
|
|
580
|
+
root := t.TempDir()
|
|
581
|
+
if err := os.Mkdir(filepath.Join(root, ".git"), 0o755); err != nil {
|
|
582
|
+
t.Fatal(err)
|
|
583
|
+
}
|
|
584
|
+
nested := filepath.Join(root, "packages", "app")
|
|
585
|
+
if err := os.MkdirAll(nested, 0o755); err != nil {
|
|
586
|
+
t.Fatal(err)
|
|
587
|
+
}
|
|
588
|
+
previous, err := os.Getwd()
|
|
589
|
+
if err != nil {
|
|
590
|
+
t.Fatal(err)
|
|
591
|
+
}
|
|
592
|
+
defer func() {
|
|
593
|
+
if err := os.Chdir(previous); err != nil {
|
|
594
|
+
t.Fatal(err)
|
|
595
|
+
}
|
|
596
|
+
}()
|
|
597
|
+
if err := os.Chdir(nested); err != nil {
|
|
598
|
+
t.Fatal(err)
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
if err := Run([]string{"start", "loop", "anchor", "state"}, &bytes.Buffer{}, &bytes.Buffer{}); err != nil {
|
|
602
|
+
t.Fatal(err)
|
|
603
|
+
}
|
|
604
|
+
projectRoot, err := projectRoot(".")
|
|
605
|
+
if err != nil {
|
|
606
|
+
t.Fatal(err)
|
|
607
|
+
}
|
|
608
|
+
workspace, err := ResolveWorkspace(".", "")
|
|
609
|
+
if err != nil {
|
|
610
|
+
t.Fatal(err)
|
|
611
|
+
}
|
|
612
|
+
if filepath.Dir(workspace) != filepath.Join(projectRoot, ".kkt", "loop") {
|
|
613
|
+
t.Fatalf("workspace = %q, want parent under project root .kkt/loop", workspace)
|
|
614
|
+
}
|
|
615
|
+
if _, err := os.Stat(filepath.Join(nested, ".kkt")); !os.IsNotExist(err) {
|
|
616
|
+
t.Fatalf("nested .kkt should not exist: %v", err)
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
func TestRunLoopCommandLifecycle(t *testing.T) {
|
|
621
|
+
root := t.TempDir()
|
|
622
|
+
previous, err := os.Getwd()
|
|
623
|
+
if err != nil {
|
|
624
|
+
t.Fatal(err)
|
|
625
|
+
}
|
|
626
|
+
defer func() {
|
|
627
|
+
if err := os.Chdir(previous); err != nil {
|
|
628
|
+
t.Fatal(err)
|
|
629
|
+
}
|
|
630
|
+
}()
|
|
631
|
+
if err := os.Chdir(root); err != nil {
|
|
632
|
+
t.Fatal(err)
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
commands := [][]string{
|
|
636
|
+
{"start", "loop", "upgrade", "kkt", "loop"},
|
|
637
|
+
{"intent", "--method", "goal_anti_goal", "Captured loop goal"},
|
|
638
|
+
{"discovery", "--method", "traceability_matrix", "Mapped loop state"},
|
|
639
|
+
{"model", "--method", "lexicographic", "Selected loop plan"},
|
|
640
|
+
{"plan", "Execute the selected loop plan."},
|
|
641
|
+
{"guardrails", "set", testGuardrailsJSON("loop", []string{"**"}, nil)},
|
|
642
|
+
{"approve", "Approved test loop"},
|
|
643
|
+
{"task", "add", "Inspect code"},
|
|
644
|
+
{"task", "start", "inspect-code"},
|
|
645
|
+
{"progress", "Started inspection"},
|
|
646
|
+
{"criteria", "add", "Evidence recorded"},
|
|
647
|
+
{"evidence", "--for", "evidence-recorded", "--command", "go test ./...", "go test ./... passed"},
|
|
648
|
+
{"criteria", "satisfy", "evidence-recorded"},
|
|
649
|
+
{"task", "done", "inspect-code"},
|
|
650
|
+
{"validate"},
|
|
651
|
+
{"done", "Loop complete"},
|
|
652
|
+
}
|
|
653
|
+
for _, command := range commands {
|
|
654
|
+
t.Run(strings.Join(command, " "), func(t *testing.T) {
|
|
655
|
+
if err := Run(command, &bytes.Buffer{}, &bytes.Buffer{}); err != nil {
|
|
656
|
+
t.Fatalf("Run(%v) error = %v", command, err)
|
|
657
|
+
}
|
|
658
|
+
})
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
workspace, err := ResolveWorkspace(".", "")
|
|
662
|
+
if err != nil {
|
|
663
|
+
t.Fatal(err)
|
|
664
|
+
}
|
|
665
|
+
state, err := os.ReadFile(filepath.Join(workspace, "kkt.yaml"))
|
|
666
|
+
if err != nil {
|
|
667
|
+
t.Fatal(err)
|
|
668
|
+
}
|
|
669
|
+
if text := string(state); !strings.Contains(text, "status: \"complete\"") || !strings.Contains(text, "current_task: \"\"") {
|
|
670
|
+
t.Fatalf("unexpected final state:\n%s", text)
|
|
671
|
+
}
|
|
672
|
+
events, err := os.ReadFile(filepath.Join(workspace, "events.jsonl"))
|
|
673
|
+
if err != nil {
|
|
674
|
+
t.Fatal(err)
|
|
675
|
+
}
|
|
676
|
+
if text := string(events); !strings.Contains(text, "task_added") || !strings.Contains(text, "done") {
|
|
677
|
+
t.Fatalf("events log missing lifecycle entries:\n%s", text)
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
func TestRunNextForFreshLoopUsesActiveLayer(t *testing.T) {
|
|
682
|
+
root := t.TempDir()
|
|
683
|
+
previous, err := os.Getwd()
|
|
684
|
+
if err != nil {
|
|
685
|
+
t.Fatal(err)
|
|
686
|
+
}
|
|
687
|
+
defer func() {
|
|
688
|
+
if err := os.Chdir(previous); err != nil {
|
|
689
|
+
t.Fatal(err)
|
|
690
|
+
}
|
|
691
|
+
}()
|
|
692
|
+
if err := os.Chdir(root); err != nil {
|
|
693
|
+
t.Fatal(err)
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
if err := Run([]string{"start", "loop", "bootstrap", "guidance"}, &bytes.Buffer{}, &bytes.Buffer{}); err != nil {
|
|
697
|
+
t.Fatal(err)
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
var textNext bytes.Buffer
|
|
701
|
+
if err := Run([]string{"next"}, &textNext, &bytes.Buffer{}); err != nil {
|
|
702
|
+
t.Fatal(err)
|
|
703
|
+
}
|
|
704
|
+
if text := textNext.String(); !strings.Contains(text, "record adaptive intent") || strings.Contains(text, "validate, then kkt done") {
|
|
705
|
+
t.Fatalf("fresh loop next should use active layer guidance:\n%s", text)
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
var jsonNext bytes.Buffer
|
|
709
|
+
if err := Run([]string{"next", "--json"}, &jsonNext, &bytes.Buffer{}); err != nil {
|
|
710
|
+
t.Fatal(err)
|
|
711
|
+
}
|
|
712
|
+
if text := jsonNext.String(); !strings.Contains(text, `"action": "continue_layer"`) || !strings.Contains(text, `"reason": "active layer is intent"`) {
|
|
713
|
+
t.Fatalf("fresh loop next --json should continue the active layer:\n%s", text)
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
func TestRunNextRequiresApprovalAfterLoopModel(t *testing.T) {
|
|
718
|
+
root := t.TempDir()
|
|
719
|
+
previous, err := os.Getwd()
|
|
720
|
+
if err != nil {
|
|
721
|
+
t.Fatal(err)
|
|
722
|
+
}
|
|
723
|
+
defer func() {
|
|
724
|
+
if err := os.Chdir(previous); err != nil {
|
|
725
|
+
t.Fatal(err)
|
|
726
|
+
}
|
|
727
|
+
}()
|
|
728
|
+
if err := os.Chdir(root); err != nil {
|
|
729
|
+
t.Fatal(err)
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
commands := [][]string{
|
|
733
|
+
{"start", "loop", "approval", "after", "model"},
|
|
734
|
+
{"intent", "--method", "goal_anti_goal", "Captured goal and anti-goal"},
|
|
735
|
+
{"discovery", "--method", "traceability_matrix", "Mapped implementation surfaces"},
|
|
736
|
+
{"model", "--method", "lexicographic", "Selected feasible plan"},
|
|
737
|
+
}
|
|
738
|
+
for _, command := range commands {
|
|
739
|
+
if err := Run(command, &bytes.Buffer{}, &bytes.Buffer{}); err != nil {
|
|
740
|
+
t.Fatalf("Run(%v) error = %v", command, err)
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
var next bytes.Buffer
|
|
745
|
+
if err := Run([]string{"next", "--json"}, &next, &bytes.Buffer{}); err != nil {
|
|
746
|
+
t.Fatal(err)
|
|
747
|
+
}
|
|
748
|
+
text := next.String()
|
|
749
|
+
if !strings.Contains(text, `"action": "request_approval"`) || !strings.Contains(text, `"blocked": true`) {
|
|
750
|
+
t.Fatalf("loop after model should request approval:\n%s", text)
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
func TestRunNextRequiresApprovalBeforeLoopTaskExecution(t *testing.T) {
|
|
755
|
+
root := t.TempDir()
|
|
756
|
+
previous, err := os.Getwd()
|
|
757
|
+
if err != nil {
|
|
758
|
+
t.Fatal(err)
|
|
759
|
+
}
|
|
760
|
+
defer func() {
|
|
761
|
+
if err := os.Chdir(previous); err != nil {
|
|
762
|
+
t.Fatal(err)
|
|
763
|
+
}
|
|
764
|
+
}()
|
|
765
|
+
if err := os.Chdir(root); err != nil {
|
|
766
|
+
t.Fatal(err)
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
commands := [][]string{
|
|
770
|
+
{"start", "loop", "approval", "gate"},
|
|
771
|
+
{"task", "add", "Inspect code"},
|
|
772
|
+
}
|
|
773
|
+
for _, command := range commands {
|
|
774
|
+
if err := Run(command, &bytes.Buffer{}, &bytes.Buffer{}); err != nil {
|
|
775
|
+
t.Fatalf("Run(%v) error = %v", command, err)
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
var next bytes.Buffer
|
|
780
|
+
if err := Run([]string{"next", "--json"}, &next, &bytes.Buffer{}); err != nil {
|
|
781
|
+
t.Fatal(err)
|
|
782
|
+
}
|
|
783
|
+
text := next.String()
|
|
784
|
+
if !strings.Contains(text, `"action": "request_approval"`) || !strings.Contains(text, `"blocked": true`) {
|
|
785
|
+
t.Fatalf("unapproved loop with work should request approval:\n%s", text)
|
|
786
|
+
}
|
|
787
|
+
if strings.Contains(text, `"action": "start_task"`) || strings.Contains(text, `"task_id": "inspect-code"`) {
|
|
788
|
+
t.Fatalf("unapproved loop should not suggest task execution:\n%s", text)
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
func TestRunNextJSONAndReplayCheck(t *testing.T) {
|
|
793
|
+
root := t.TempDir()
|
|
794
|
+
previous, err := os.Getwd()
|
|
795
|
+
if err != nil {
|
|
796
|
+
t.Fatal(err)
|
|
797
|
+
}
|
|
798
|
+
defer func() {
|
|
799
|
+
if err := os.Chdir(previous); err != nil {
|
|
800
|
+
t.Fatal(err)
|
|
801
|
+
}
|
|
802
|
+
}()
|
|
803
|
+
if err := os.Chdir(root); err != nil {
|
|
804
|
+
t.Fatal(err)
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
commands := [][]string{
|
|
808
|
+
{"start", "loop", "add", "replay", "check"},
|
|
809
|
+
{"approve", "Approved replay check"},
|
|
810
|
+
{"task", "add", "Inspect code"},
|
|
811
|
+
}
|
|
812
|
+
for _, command := range commands {
|
|
813
|
+
if err := Run(command, &bytes.Buffer{}, &bytes.Buffer{}); err != nil {
|
|
814
|
+
t.Fatalf("Run(%v) error = %v", command, err)
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
var next bytes.Buffer
|
|
819
|
+
if err := Run([]string{"next", "--json"}, &next, &bytes.Buffer{}); err != nil {
|
|
820
|
+
t.Fatal(err)
|
|
821
|
+
}
|
|
822
|
+
if text := next.String(); !strings.Contains(text, `"action": "start_task"`) || !strings.Contains(text, `"task_id": "inspect-code"`) {
|
|
823
|
+
t.Fatalf("next --json output did not include structured task action:\n%s", text)
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
if err := Run([]string{"replay", "--check"}, &bytes.Buffer{}, &bytes.Buffer{}); err != nil {
|
|
827
|
+
t.Fatalf("replay --check should pass for consistent event/state history: %v", err)
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
func TestRunResumeIncludesContinuationPacket(t *testing.T) {
|
|
832
|
+
root := t.TempDir()
|
|
833
|
+
previous, err := os.Getwd()
|
|
834
|
+
if err != nil {
|
|
835
|
+
t.Fatal(err)
|
|
836
|
+
}
|
|
837
|
+
defer func() {
|
|
838
|
+
if err := os.Chdir(previous); err != nil {
|
|
839
|
+
t.Fatal(err)
|
|
840
|
+
}
|
|
841
|
+
}()
|
|
842
|
+
if err := os.Chdir(root); err != nil {
|
|
843
|
+
t.Fatal(err)
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
commands := [][]string{
|
|
847
|
+
{"start", "loop", "resume", "context"},
|
|
848
|
+
{"approve", "Approved resume check"},
|
|
849
|
+
{"task", "add", "Inspect code"},
|
|
850
|
+
{"task", "start", "inspect-code"},
|
|
851
|
+
{"criteria", "add", "Evidence recorded"},
|
|
852
|
+
{"evidence", "--for", "evidence-recorded", "Inspection evidence recorded"},
|
|
853
|
+
}
|
|
854
|
+
for _, command := range commands {
|
|
855
|
+
if err := Run(command, &bytes.Buffer{}, &bytes.Buffer{}); err != nil {
|
|
856
|
+
t.Fatalf("Run(%v) error = %v", command, err)
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
var resume bytes.Buffer
|
|
861
|
+
if err := Run([]string{"resume"}, &resume, &bytes.Buffer{}); err != nil {
|
|
862
|
+
t.Fatal(err)
|
|
863
|
+
}
|
|
864
|
+
text := resume.String()
|
|
865
|
+
for _, want := range []string{"approval: approved", "current_task: inspect-code", "unsatisfied_criteria:", "latest_evidence:", "recent_events:", "validation: invalid"} {
|
|
866
|
+
if !strings.Contains(text, want) {
|
|
867
|
+
t.Fatalf("resume output missing %q:\n%s", want, text)
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
func TestRunDoneRequiresApprovalAndMappedEvidence(t *testing.T) {
|
|
873
|
+
root := t.TempDir()
|
|
874
|
+
previous, err := os.Getwd()
|
|
875
|
+
if err != nil {
|
|
876
|
+
t.Fatal(err)
|
|
877
|
+
}
|
|
878
|
+
defer func() {
|
|
879
|
+
if err := os.Chdir(previous); err != nil {
|
|
880
|
+
t.Fatal(err)
|
|
881
|
+
}
|
|
882
|
+
}()
|
|
883
|
+
if err := os.Chdir(root); err != nil {
|
|
884
|
+
t.Fatal(err)
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
commands := [][]string{
|
|
888
|
+
{"start", "loop", "prove", "terminal", "validation"},
|
|
889
|
+
{"task", "add", "Inspect code"},
|
|
890
|
+
{"task", "start", "inspect-code"},
|
|
891
|
+
{"criteria", "add", "Evidence recorded"},
|
|
892
|
+
{"evidence", "Unmapped evidence"},
|
|
893
|
+
{"criteria", "satisfy", "evidence-recorded"},
|
|
894
|
+
{"task", "done", "inspect-code"},
|
|
895
|
+
}
|
|
896
|
+
for _, command := range commands {
|
|
897
|
+
if err := Run(command, &bytes.Buffer{}, &bytes.Buffer{}); err != nil {
|
|
898
|
+
t.Fatalf("Run(%v) error = %v", command, err)
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
var stdout bytes.Buffer
|
|
903
|
+
err = Run([]string{"done"}, &stdout, &bytes.Buffer{})
|
|
904
|
+
if err == nil {
|
|
905
|
+
t.Fatal("done should fail without approval and mapped evidence")
|
|
906
|
+
}
|
|
907
|
+
text := stdout.String()
|
|
908
|
+
if !strings.Contains(text, "approval is not approved") || !strings.Contains(text, "criterion evidence-recorded is satisfied without mapped evidence") {
|
|
909
|
+
t.Fatalf("done output missing terminal invariant failures:\n%s", text)
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
func TestValidateReportsMissingRequiredCommandProof(t *testing.T) {
|
|
914
|
+
root := t.TempDir()
|
|
915
|
+
initGit(t, root)
|
|
916
|
+
previous, err := os.Getwd()
|
|
917
|
+
if err != nil {
|
|
918
|
+
t.Fatal(err)
|
|
919
|
+
}
|
|
920
|
+
defer func() {
|
|
921
|
+
if err := os.Chdir(previous); err != nil {
|
|
922
|
+
t.Fatal(err)
|
|
923
|
+
}
|
|
924
|
+
}()
|
|
925
|
+
if err := os.Chdir(root); err != nil {
|
|
926
|
+
t.Fatal(err)
|
|
927
|
+
}
|
|
928
|
+
startValidationRunWorkspace(t, []string{"printf ok"})
|
|
929
|
+
|
|
930
|
+
var stdout bytes.Buffer
|
|
931
|
+
err = Run([]string{"validate"}, &stdout, &bytes.Buffer{})
|
|
932
|
+
if err == nil {
|
|
933
|
+
t.Fatal("validate should fail when a required command has no proof")
|
|
934
|
+
}
|
|
935
|
+
if text := stdout.String(); !strings.Contains(text, "required command not run: printf ok") || !strings.Contains(text, "kkt validate --run") {
|
|
936
|
+
t.Fatalf("validate output missing command proof issue:\n%s", text)
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
func TestValidateModelDoesNotRequireCommandProof(t *testing.T) {
|
|
941
|
+
root := t.TempDir()
|
|
942
|
+
initGit(t, root)
|
|
943
|
+
previous, err := os.Getwd()
|
|
944
|
+
if err != nil {
|
|
945
|
+
t.Fatal(err)
|
|
946
|
+
}
|
|
947
|
+
defer func() {
|
|
948
|
+
if err := os.Chdir(previous); err != nil {
|
|
949
|
+
t.Fatal(err)
|
|
950
|
+
}
|
|
951
|
+
}()
|
|
952
|
+
if err := os.Chdir(root); err != nil {
|
|
953
|
+
t.Fatal(err)
|
|
954
|
+
}
|
|
955
|
+
startValidationModelWorkspace(t, []string{"printf ok"})
|
|
956
|
+
|
|
957
|
+
var stdout bytes.Buffer
|
|
958
|
+
if err := Run([]string{"validate"}, &stdout, &bytes.Buffer{}); err != nil {
|
|
959
|
+
t.Fatalf("model validate should not require command proof: %v\n%s", err, stdout.String())
|
|
960
|
+
}
|
|
961
|
+
if text := stdout.String(); !strings.Contains(text, "valid:") {
|
|
962
|
+
t.Fatalf("model validate output missing valid status:\n%s", text)
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
func TestValidateRunRecordsRequiredCommandProof(t *testing.T) {
|
|
967
|
+
root := t.TempDir()
|
|
968
|
+
initGit(t, root)
|
|
969
|
+
previous, err := os.Getwd()
|
|
970
|
+
if err != nil {
|
|
971
|
+
t.Fatal(err)
|
|
972
|
+
}
|
|
973
|
+
defer func() {
|
|
974
|
+
if err := os.Chdir(previous); err != nil {
|
|
975
|
+
t.Fatal(err)
|
|
976
|
+
}
|
|
977
|
+
}()
|
|
978
|
+
if err := os.Chdir(root); err != nil {
|
|
979
|
+
t.Fatal(err)
|
|
980
|
+
}
|
|
981
|
+
workspace := startValidationRunWorkspace(t, []string{"printf ok"})
|
|
982
|
+
|
|
983
|
+
var stdout bytes.Buffer
|
|
984
|
+
if err := Run([]string{"validate", "--run", "--timeout", "5s"}, &stdout, &bytes.Buffer{}); err != nil {
|
|
985
|
+
t.Fatalf("validate --run error = %v\n%s", err, stdout.String())
|
|
986
|
+
}
|
|
987
|
+
if text := stdout.String(); !strings.Contains(text, "passed: printf ok") || !strings.Contains(text, "valid:") {
|
|
988
|
+
t.Fatalf("validate --run output missing pass and valid status:\n%s", text)
|
|
989
|
+
}
|
|
990
|
+
events, err := readEvents(workspace, 0)
|
|
991
|
+
if err != nil {
|
|
992
|
+
t.Fatal(err)
|
|
993
|
+
}
|
|
994
|
+
var sawCommandProof bool
|
|
995
|
+
for _, event := range events {
|
|
996
|
+
if event.Type == "validation_command_passed" && event.Data["command"] == "printf ok" {
|
|
997
|
+
sawCommandProof = true
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
if !sawCommandProof {
|
|
1001
|
+
t.Fatalf("events missing validation_command_passed proof: %#v", events)
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
func TestValidateRunFailsWhenRequiredCommandFails(t *testing.T) {
|
|
1006
|
+
root := t.TempDir()
|
|
1007
|
+
initGit(t, root)
|
|
1008
|
+
previous, err := os.Getwd()
|
|
1009
|
+
if err != nil {
|
|
1010
|
+
t.Fatal(err)
|
|
1011
|
+
}
|
|
1012
|
+
defer func() {
|
|
1013
|
+
if err := os.Chdir(previous); err != nil {
|
|
1014
|
+
t.Fatal(err)
|
|
1015
|
+
}
|
|
1016
|
+
}()
|
|
1017
|
+
if err := os.Chdir(root); err != nil {
|
|
1018
|
+
t.Fatal(err)
|
|
1019
|
+
}
|
|
1020
|
+
startValidationRunWorkspace(t, []string{"false"})
|
|
1021
|
+
|
|
1022
|
+
var stdout bytes.Buffer
|
|
1023
|
+
err = Run([]string{"validate", "--run", "--timeout", "5s"}, &stdout, &bytes.Buffer{})
|
|
1024
|
+
if err == nil {
|
|
1025
|
+
t.Fatal("validate --run should fail when a required command fails")
|
|
1026
|
+
}
|
|
1027
|
+
if text := stdout.String(); !strings.Contains(text, "failed: false") || !strings.Contains(text, "log:") {
|
|
1028
|
+
t.Fatalf("validate --run output missing failure details:\n%s", text)
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
func TestValidateReportsStaleRequiredCommandProof(t *testing.T) {
|
|
1033
|
+
root := t.TempDir()
|
|
1034
|
+
initGit(t, root)
|
|
1035
|
+
previous, err := os.Getwd()
|
|
1036
|
+
if err != nil {
|
|
1037
|
+
t.Fatal(err)
|
|
1038
|
+
}
|
|
1039
|
+
defer func() {
|
|
1040
|
+
if err := os.Chdir(previous); err != nil {
|
|
1041
|
+
t.Fatal(err)
|
|
1042
|
+
}
|
|
1043
|
+
}()
|
|
1044
|
+
if err := os.Chdir(root); err != nil {
|
|
1045
|
+
t.Fatal(err)
|
|
1046
|
+
}
|
|
1047
|
+
startValidationRunWorkspace(t, []string{"true"})
|
|
1048
|
+
|
|
1049
|
+
if err := Run([]string{"validate", "--run", "--timeout", "5s"}, &bytes.Buffer{}, &bytes.Buffer{}); err != nil {
|
|
1050
|
+
t.Fatal(err)
|
|
1051
|
+
}
|
|
1052
|
+
if err := os.WriteFile(filepath.Join(root, "changed.txt"), []byte("changed\n"), 0o644); err != nil {
|
|
1053
|
+
t.Fatal(err)
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
var stdout bytes.Buffer
|
|
1057
|
+
err = Run([]string{"validate"}, &stdout, &bytes.Buffer{})
|
|
1058
|
+
if err == nil {
|
|
1059
|
+
t.Fatal("validate should fail when command proof is stale")
|
|
1060
|
+
}
|
|
1061
|
+
if text := stdout.String(); !strings.Contains(text, "required command proof is stale: true") {
|
|
1062
|
+
t.Fatalf("validate output missing stale proof issue:\n%s", text)
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
func TestValidateIgnoresUnrelatedChangedPathForRequiredCommandProof(t *testing.T) {
|
|
1067
|
+
root := t.TempDir()
|
|
1068
|
+
initGit(t, root)
|
|
1069
|
+
previous, err := os.Getwd()
|
|
1070
|
+
if err != nil {
|
|
1071
|
+
t.Fatal(err)
|
|
1072
|
+
}
|
|
1073
|
+
defer func() {
|
|
1074
|
+
if err := os.Chdir(previous); err != nil {
|
|
1075
|
+
t.Fatal(err)
|
|
1076
|
+
}
|
|
1077
|
+
}()
|
|
1078
|
+
if err := os.Chdir(root); err != nil {
|
|
1079
|
+
t.Fatal(err)
|
|
1080
|
+
}
|
|
1081
|
+
startValidationRunWorkspaceWithBounds(t, []string{"true"}, []string{"internal/workflow/**"}, nil)
|
|
1082
|
+
|
|
1083
|
+
if err := Run([]string{"validate", "--run", "--timeout", "5s"}, &bytes.Buffer{}, &bytes.Buffer{}); err != nil {
|
|
1084
|
+
t.Fatal(err)
|
|
1085
|
+
}
|
|
1086
|
+
if err := os.WriteFile(filepath.Join(root, "README.md"), []byte("unrelated\n"), 0o644); err != nil {
|
|
1087
|
+
t.Fatal(err)
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
var stdout bytes.Buffer
|
|
1091
|
+
if err := Run([]string{"validate"}, &stdout, &bytes.Buffer{}); err != nil {
|
|
1092
|
+
t.Fatalf("validate should ignore unrelated changed path: %v\n%s", err, stdout.String())
|
|
1093
|
+
}
|
|
1094
|
+
if text := stdout.String(); !strings.Contains(text, "valid:") || strings.Contains(text, "required command proof is stale") {
|
|
1095
|
+
t.Fatalf("validate should keep command proof fresh for unrelated change:\n%s", text)
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
func TestStatusReportsStaleCompleteWorkspace(t *testing.T) {
|
|
1100
|
+
root := t.TempDir()
|
|
1101
|
+
initGit(t, root)
|
|
1102
|
+
previous, err := os.Getwd()
|
|
1103
|
+
if err != nil {
|
|
1104
|
+
t.Fatal(err)
|
|
1105
|
+
}
|
|
1106
|
+
defer func() {
|
|
1107
|
+
if err := os.Chdir(previous); err != nil {
|
|
1108
|
+
t.Fatal(err)
|
|
1109
|
+
}
|
|
1110
|
+
}()
|
|
1111
|
+
if err := os.Chdir(root); err != nil {
|
|
1112
|
+
t.Fatal(err)
|
|
1113
|
+
}
|
|
1114
|
+
startValidationRunWorkspace(t, []string{"true"})
|
|
1115
|
+
if err := Run([]string{"validate", "--run", "--timeout", "5s"}, &bytes.Buffer{}, &bytes.Buffer{}); err != nil {
|
|
1116
|
+
t.Fatal(err)
|
|
1117
|
+
}
|
|
1118
|
+
if err := Run([]string{"done", "Run complete"}, &bytes.Buffer{}, &bytes.Buffer{}); err != nil {
|
|
1119
|
+
t.Fatal(err)
|
|
1120
|
+
}
|
|
1121
|
+
if err := os.WriteFile(filepath.Join(root, "changed.txt"), []byte("changed\n"), 0o644); err != nil {
|
|
1122
|
+
t.Fatal(err)
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
var status bytes.Buffer
|
|
1126
|
+
if err := Run([]string{"status", "--json"}, &status, &bytes.Buffer{}); err != nil {
|
|
1127
|
+
t.Fatal(err)
|
|
1128
|
+
}
|
|
1129
|
+
text := status.String()
|
|
1130
|
+
if !strings.Contains(text, `"stale_complete": true`) || !strings.Contains(text, `"action": "repair_invalid_completion"`) {
|
|
1131
|
+
t.Fatalf("status --json should report stale complete repair action:\n%s", text)
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
func TestJudgeFinalizeBlocksMissingRequiredCommandProof(t *testing.T) {
|
|
1136
|
+
root := t.TempDir()
|
|
1137
|
+
initGit(t, root)
|
|
1138
|
+
previous, err := os.Getwd()
|
|
1139
|
+
if err != nil {
|
|
1140
|
+
t.Fatal(err)
|
|
1141
|
+
}
|
|
1142
|
+
defer func() {
|
|
1143
|
+
if err := os.Chdir(previous); err != nil {
|
|
1144
|
+
t.Fatal(err)
|
|
1145
|
+
}
|
|
1146
|
+
}()
|
|
1147
|
+
if err := os.Chdir(root); err != nil {
|
|
1148
|
+
t.Fatal(err)
|
|
1149
|
+
}
|
|
1150
|
+
startValidationRunWorkspace(t, []string{"printf ok"})
|
|
1151
|
+
|
|
1152
|
+
var stdout bytes.Buffer
|
|
1153
|
+
err = Run([]string{"judge", "--checkpoint", "finalize", "--json"}, &stdout, &bytes.Buffer{})
|
|
1154
|
+
if err == nil {
|
|
1155
|
+
t.Fatal("finalize judge should block when required command proof is missing")
|
|
1156
|
+
}
|
|
1157
|
+
if text := stdout.String(); !strings.Contains(text, `"verdict": "block"`) || !strings.Contains(text, "required command not run: printf ok") {
|
|
1158
|
+
t.Fatalf("judge output missing validation proof issue:\n%s", text)
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
func startValidationModelWorkspace(t *testing.T, requiredCommands []string) string {
|
|
1163
|
+
t.Helper()
|
|
1164
|
+
commands := [][]string{
|
|
1165
|
+
{"start", "model", "choose", "API", "shape"},
|
|
1166
|
+
{"intent", "--method", "why_how", "Clarified owner tradeoffs"},
|
|
1167
|
+
{"discovery", "--method", "coupling_map", "Mapped affected API callers"},
|
|
1168
|
+
{"model", "--method", "ordinal_mcda", "Compared feasible API shapes"},
|
|
1169
|
+
{"guardrails", "set", testGuardrailsJSONWithCommands("model", []string{"**"}, nil, requiredCommands)},
|
|
1170
|
+
}
|
|
1171
|
+
for _, command := range commands {
|
|
1172
|
+
if err := Run(command, &bytes.Buffer{}, &bytes.Buffer{}); err != nil {
|
|
1173
|
+
t.Fatalf("Run(%v) error = %v", command, err)
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
workspace, err := ResolveWorkspace(".", "")
|
|
1177
|
+
if err != nil {
|
|
1178
|
+
t.Fatal(err)
|
|
1179
|
+
}
|
|
1180
|
+
return workspace
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
func startValidationRunWorkspace(t *testing.T, requiredCommands []string) string {
|
|
1184
|
+
t.Helper()
|
|
1185
|
+
return startValidationRunWorkspaceWithBounds(t, requiredCommands, []string{"**"}, nil)
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
func startValidationRunWorkspaceWithBounds(t *testing.T, requiredCommands, allowedPaths, blockedPaths []string) string {
|
|
1189
|
+
t.Helper()
|
|
1190
|
+
commands := [][]string{
|
|
1191
|
+
{"start", "run", "execute", "selected", "model"},
|
|
1192
|
+
{"intent", "--method", "why_how", "Clarified owner tradeoffs"},
|
|
1193
|
+
{"discovery", "--method", "coupling_map", "Mapped affected API callers"},
|
|
1194
|
+
{"model", "--method", "ordinal_mcda", "Compared feasible API shapes"},
|
|
1195
|
+
{"guardrails", "set", testGuardrailsJSONWithCommands("run", allowedPaths, blockedPaths, requiredCommands)},
|
|
1196
|
+
{"approve", "Approved validation run"},
|
|
1197
|
+
{"plan", "Run selected validation model."},
|
|
1198
|
+
{"evidence", "Narrative evidence recorded."},
|
|
1199
|
+
}
|
|
1200
|
+
for _, command := range commands {
|
|
1201
|
+
if err := Run(command, &bytes.Buffer{}, &bytes.Buffer{}); err != nil {
|
|
1202
|
+
t.Fatalf("Run(%v) error = %v", command, err)
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
workspace, err := ResolveWorkspace(".", "")
|
|
1206
|
+
if err != nil {
|
|
1207
|
+
t.Fatal(err)
|
|
1208
|
+
}
|
|
1209
|
+
return workspace
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
func testGuardrailsJSON(executionMode string, allowedPaths, blockedPaths []string) string {
|
|
1213
|
+
return testGuardrailsJSONWithCommands(executionMode, allowedPaths, blockedPaths, nil)
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
func testGuardrailsJSONWithCommands(executionMode string, allowedPaths, blockedPaths, requiredCommands []string) string {
|
|
1217
|
+
payload := map[string]any{
|
|
1218
|
+
"schema_version": 1,
|
|
1219
|
+
"source": map[string]any{
|
|
1220
|
+
"workspace_type": executionMode,
|
|
1221
|
+
"workspace": ".kkt/test-workspace",
|
|
1222
|
+
"request": "test request",
|
|
1223
|
+
},
|
|
1224
|
+
"constraints": []map[string]any{
|
|
1225
|
+
{
|
|
1226
|
+
"id": "test-scope",
|
|
1227
|
+
"kind": "scope",
|
|
1228
|
+
"severity": "block",
|
|
1229
|
+
"statement": "Stay inside the modeled test scope.",
|
|
1230
|
+
"allowed_paths": allowedPaths,
|
|
1231
|
+
"blocked_paths": blockedPaths,
|
|
1232
|
+
},
|
|
1233
|
+
},
|
|
1234
|
+
"change_bounds": map[string]any{
|
|
1235
|
+
"allowed_paths": allowedPaths,
|
|
1236
|
+
"blocked_paths": blockedPaths,
|
|
1237
|
+
},
|
|
1238
|
+
"workflow": map[string]any{
|
|
1239
|
+
"execution_mode": executionMode,
|
|
1240
|
+
"requires_approval_before_mutation": executionMode == "run" || executionMode == "loop",
|
|
1241
|
+
"requires_validation_before_done": true,
|
|
1242
|
+
},
|
|
1243
|
+
"validation": map[string]any{
|
|
1244
|
+
"acceptance_criteria": []string{"test scope is enforced"},
|
|
1245
|
+
"required_commands": requiredCommands,
|
|
1246
|
+
"evidence_required": []string{"scope audit confirms only allowed paths changed"},
|
|
1247
|
+
},
|
|
1248
|
+
"drift_policy": map[string]any{
|
|
1249
|
+
"block_on": []string{"missing_approval", "empty_allowed_paths", "changed_blocked_path", "validation_failed"},
|
|
1250
|
+
"warn_on": []string{},
|
|
1251
|
+
},
|
|
1252
|
+
}
|
|
1253
|
+
encoded, err := json.Marshal(payload)
|
|
1254
|
+
if err != nil {
|
|
1255
|
+
panic(err)
|
|
1256
|
+
}
|
|
1257
|
+
return string(encoded)
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
func initGit(t *testing.T, root string) {
|
|
1261
|
+
t.Helper()
|
|
1262
|
+
command := exec.Command("git", "-C", root, "init")
|
|
1263
|
+
if output, err := command.CombinedOutput(); err != nil {
|
|
1264
|
+
t.Fatalf("git init failed: %v\n%s", err, output)
|
|
1265
|
+
}
|
|
1266
|
+
}
|