devrites 4.1.0 → 4.2.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/CHANGELOG.md +6 -0
- package/README.md +1 -1
- package/docs/engine/commands.md +65 -6
- package/engine/go.mod +1 -1
- package/engine/internal/gate/gate.go +154 -46
- package/engine/internal/gate/gate_test.go +290 -2
- package/engine/internal/gate/readiness_binding.go +66 -100
- package/engine/internal/gate/readiness_binding_test.go +126 -3
- package/engine/internal/lib/resolve.go +6 -6
- package/engine/internal/state/cmd/workflowmanifest/main.go +32 -3
- package/engine/internal/state/feature.go +1 -100
- package/engine/internal/state/observation.go +584 -0
- package/engine/internal/state/observation_open_other.go +9 -0
- package/engine/internal/state/observation_open_unix.go +12 -0
- package/engine/internal/state/observation_test.go +960 -0
- package/engine/internal/state/observation_unix_test.go +68 -0
- package/engine/internal/state/schema.go +60 -160
- package/engine/internal/state/state_test.go +489 -101
- package/engine/internal/state/status.go +67 -48
- package/engine/tests/adr_0004_required_by_phase_test.go +16 -13
- package/engine/tests/adr_0011_define_plan_test.go +14 -12
- package/engine/tests/gate_test.go +403 -28
- package/engine/tests/workspace_observation_migration_test.go +1065 -0
- package/pack/.claude/skills/devrites-debug-recovery/SKILL.md +1 -10
- package/pack/.claude/skills/devrites-lib/reference/standards/acceptance-preserving-reslice.md +30 -0
- package/pack/.claude/skills/devrites-lib/reference/standards/afk-hitl.md +2 -9
- package/pack/.claude/skills/devrites-lib/reference/standards/one-shot-actions.md +1 -6
- package/pack/.claude/skills/devrites-lib/reference/standards/workflow-artifacts.md +411 -102
- package/pack/.claude/skills/rite-autocomplete/SKILL.md +98 -146
- package/pack/.claude/skills/rite-autocomplete/reference/decision-policy.md +11 -5
- package/pack/.claude/skills/rite-autocomplete/reference/loop.md +96 -134
- package/pack/.claude/skills/rite-autocomplete/reference/stop-conditions.md +92 -131
- package/pack/.claude/skills/rite-build/SKILL.md +51 -79
- package/pack/.claude/skills/rite-build/reference/phase-contract.md +3 -17
- package/pack/.claude/skills/rite-plan/SKILL.md +17 -17
- package/pack/.claude/skills/rite-plan/reference/anti-patterns.md +11 -1
- package/pack/.claude/skills/rite-plan/reference/replan-and-repair.md +10 -3
- package/pack/.claude/skills/rite-prove/SKILL.md +95 -153
- package/pack/.claude/skills/rite-vet/SKILL.md +121 -174
- package/pack/.claude/skills/rite-vet/reference/anti-patterns.md +14 -2
- package/pack/.claude/skills/rite-vet/reference/artifacts.md +10 -6
- package/pack/.claude/skills/rite-vet/reference/depth.md +10 -3
- package/pack/.claude/skills/rite-vet/reference/review-axes.md +12 -4
- package/pack/generated/claude/skills/devrites-debug-recovery/SKILL.md +1 -10
- package/pack/generated/claude/skills/devrites-lib/reference/standards/acceptance-preserving-reslice.md +30 -0
- package/pack/generated/claude/skills/devrites-lib/reference/standards/afk-hitl.md +2 -9
- package/pack/generated/claude/skills/devrites-lib/reference/standards/one-shot-actions.md +1 -6
- package/pack/generated/claude/skills/devrites-lib/reference/standards/workflow-artifacts.md +411 -102
- package/pack/generated/claude/skills/rite-autocomplete/SKILL.md +98 -146
- package/pack/generated/claude/skills/rite-autocomplete/reference/decision-policy.md +11 -5
- package/pack/generated/claude/skills/rite-autocomplete/reference/loop.md +96 -134
- package/pack/generated/claude/skills/rite-autocomplete/reference/stop-conditions.md +92 -131
- package/pack/generated/claude/skills/rite-build/SKILL.md +51 -79
- package/pack/generated/claude/skills/rite-build/reference/phase-contract.md +3 -17
- package/pack/generated/claude/skills/rite-plan/SKILL.md +17 -17
- package/pack/generated/claude/skills/rite-plan/reference/anti-patterns.md +11 -1
- package/pack/generated/claude/skills/rite-plan/reference/replan-and-repair.md +10 -3
- package/pack/generated/claude/skills/rite-prove/SKILL.md +95 -153
- package/pack/generated/claude/skills/rite-vet/SKILL.md +121 -174
- package/pack/generated/claude/skills/rite-vet/reference/anti-patterns.md +14 -2
- package/pack/generated/claude/skills/rite-vet/reference/artifacts.md +10 -6
- package/pack/generated/claude/skills/rite-vet/reference/depth.md +10 -3
- package/pack/generated/claude/skills/rite-vet/reference/review-axes.md +12 -4
- package/pack/generated/codex/skills/devrites-debug-recovery/SKILL.md +1 -10
- package/pack/generated/codex/skills/devrites-lib/reference/standards/acceptance-preserving-reslice.md +30 -0
- package/pack/generated/codex/skills/devrites-lib/reference/standards/afk-hitl.md +2 -9
- package/pack/generated/codex/skills/devrites-lib/reference/standards/one-shot-actions.md +1 -6
- package/pack/generated/codex/skills/devrites-lib/reference/standards/workflow-artifacts.md +411 -102
- package/pack/generated/codex/skills/rite-autocomplete/SKILL.md +98 -146
- package/pack/generated/codex/skills/rite-autocomplete/reference/decision-policy.md +11 -5
- package/pack/generated/codex/skills/rite-autocomplete/reference/loop.md +96 -134
- package/pack/generated/codex/skills/rite-autocomplete/reference/stop-conditions.md +92 -131
- package/pack/generated/codex/skills/rite-build/SKILL.md +51 -79
- package/pack/generated/codex/skills/rite-build/reference/phase-contract.md +3 -17
- package/pack/generated/codex/skills/rite-plan/SKILL.md +17 -17
- package/pack/generated/codex/skills/rite-plan/reference/anti-patterns.md +11 -1
- package/pack/generated/codex/skills/rite-plan/reference/replan-and-repair.md +10 -3
- package/pack/generated/codex/skills/rite-prove/SKILL.md +95 -153
- package/pack/generated/codex/skills/rite-vet/SKILL.md +121 -174
- package/pack/generated/codex/skills/rite-vet/reference/anti-patterns.md +14 -2
- package/pack/generated/codex/skills/rite-vet/reference/artifacts.md +10 -6
- package/pack/generated/codex/skills/rite-vet/reference/depth.md +10 -3
- package/pack/generated/codex/skills/rite-vet/reference/review-axes.md +12 -4
- package/package.json +1 -1
|
@@ -9,37 +9,99 @@ import (
|
|
|
9
9
|
"testing"
|
|
10
10
|
"time"
|
|
11
11
|
|
|
12
|
+
internalgate "github.com/devrites/devrites/internal/gate"
|
|
12
13
|
"github.com/devrites/devrites/internal/lib"
|
|
14
|
+
"github.com/devrites/devrites/internal/state"
|
|
13
15
|
"github.com/devrites/devrites/internal/testutil"
|
|
14
16
|
)
|
|
15
17
|
|
|
16
18
|
func TestReadinessBlocksIncompletePhase(t *testing.T) {
|
|
17
19
|
root := newWorkspace(t)
|
|
18
|
-
out,
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
20
|
+
out, errOut, code := runDevrites(t, root, "check", "readiness", "auth-tokens") // build phase, tasks empty
|
|
21
|
+
wantOut := "gate: readiness\n" +
|
|
22
|
+
"feature: auth-tokens\n" +
|
|
23
|
+
"phase: build\n" +
|
|
24
|
+
"result: blocked (missing to leave \"build\": tasks.md)\n" +
|
|
25
|
+
"reason: DRV-GATE-READINESS-MISSING\n" +
|
|
26
|
+
"next: add real content to tasks.md\n" +
|
|
27
|
+
"retry: devrites-engine check readiness auth-tokens\n"
|
|
28
|
+
if code != 3 || out != wantOut || errOut != "" {
|
|
29
|
+
t.Fatalf("code=%d stdout=%q stderr=%q, want code=3 stdout=%q stderr empty", code, out, errOut, wantOut)
|
|
27
30
|
}
|
|
28
31
|
}
|
|
29
32
|
|
|
30
33
|
func TestReadinessPassesCompletePhase(t *testing.T) {
|
|
31
34
|
root := newWorkspace(t)
|
|
32
|
-
out,
|
|
33
|
-
|
|
34
|
-
|
|
35
|
+
out, errOut, code := runDevrites(t, root, "check", "readiness", "search-ranking")
|
|
36
|
+
wantOut := "gate: readiness\n" +
|
|
37
|
+
"feature: search-ranking\n" +
|
|
38
|
+
"phase: spec\n" +
|
|
39
|
+
"result: pass\n" +
|
|
40
|
+
"reason: DRV-GATE-READINESS-PASSED\n"
|
|
41
|
+
if code != 0 || out != wantOut || errOut != "" {
|
|
42
|
+
t.Fatalf("code=%d stdout=%q stderr=%q, want code=0 stdout=%q stderr empty", code, out, errOut, wantOut)
|
|
35
43
|
}
|
|
36
|
-
|
|
37
|
-
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
func TestReadinessBlocksAbsentRequiredArtifactExactCLIContract(t *testing.T) {
|
|
47
|
+
root := filepath.Join(t.TempDir(), ".devrites")
|
|
48
|
+
const slug = "absent-required"
|
|
49
|
+
writeCompleteGateCLIWorkspace(t, root, slug, state.PhaseSpec, state.PhaseSpec, "none\n")
|
|
50
|
+
if err := os.Remove(filepath.Join(root, "work", slug, "spec.md")); err != nil {
|
|
51
|
+
t.Fatal(err)
|
|
38
52
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
53
|
+
|
|
54
|
+
out, errOut, code := runDevrites(t, root, "check", "readiness", slug)
|
|
55
|
+
wantOut := "gate: readiness\n" +
|
|
56
|
+
"feature: absent-required\n" +
|
|
57
|
+
"phase: spec\n" +
|
|
58
|
+
"result: blocked (missing to leave \"spec\": spec.md)\n" +
|
|
59
|
+
"reason: DRV-GATE-READINESS-MISSING\n" +
|
|
60
|
+
"next: add real content to spec.md\n" +
|
|
61
|
+
"retry: devrites-engine check readiness absent-required\n"
|
|
62
|
+
if code != 3 || out != wantOut || errOut != "" {
|
|
63
|
+
t.Fatalf("code=%d stdout=%q stderr=%q, want code=3 stdout=%q stderr empty", code, out, errOut, wantOut)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
func TestReadinessEmitBindingPassesExactCLIContract(t *testing.T) {
|
|
68
|
+
root := filepath.Join(t.TempDir(), ".devrites")
|
|
69
|
+
const slug = "binding-success"
|
|
70
|
+
writeCompleteGateCLIWorkspace(t, root, slug, state.PhaseBuild, state.PhaseBuild, "none\n")
|
|
71
|
+
|
|
72
|
+
out, errOut, code := runDevrites(t, root, "check", "readiness", "--emit-binding", slug)
|
|
73
|
+
const wantOut = "Readiness inputs SHA-256: 71c2d192ea09bca1d2c8806cb197e7fa4d1d08e0b331cf25b2e1bcb7ecac34e4\n"
|
|
74
|
+
if code != 0 || out != wantOut || errOut != "" {
|
|
75
|
+
t.Fatalf("code=%d stdout=%q stderr=%q, want code=0 stdout=%q stderr empty", code, out, errOut, wantOut)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
func TestReadinessBlocksStaleBindingExactCLIContract(t *testing.T) {
|
|
80
|
+
root := filepath.Join(t.TempDir(), ".devrites")
|
|
81
|
+
const slug = "stale-readiness"
|
|
82
|
+
writeCompleteGateCLIWorkspace(t, root, slug, state.PhaseBuild, state.PhaseBuild, "none\n")
|
|
83
|
+
workspace := filepath.Join(root, "work", slug)
|
|
84
|
+
binding, err := internalgate.ReadinessBinding(root, slug)
|
|
85
|
+
if err != nil {
|
|
86
|
+
t.Fatal(err)
|
|
87
|
+
}
|
|
88
|
+
testutil.AppendFile(t, filepath.Join(workspace, "eng-review.md"), "\n"+binding+"\n")
|
|
89
|
+
testutil.WriteFile(t, filepath.Join(workspace, "plan.md"), "# Plan\n\nChanged after Vet.\n")
|
|
90
|
+
expected, err := internalgate.ReadinessBinding(root, slug)
|
|
91
|
+
if err != nil {
|
|
92
|
+
t.Fatal(err)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
out, errOut, code := runDevrites(t, root, "check", "readiness", slug)
|
|
96
|
+
wantOut := "gate: readiness\n" +
|
|
97
|
+
"feature: " + slug + "\n" +
|
|
98
|
+
"phase: build\n" +
|
|
99
|
+
"result: blocked (state invariant)\n" +
|
|
100
|
+
"reason: DRV-GATE-READINESS-STALE\n" +
|
|
101
|
+
"invariant: readiness inputs are stale or the binding is invalid; rerun /rite-vet and record exactly one standalone line: " + expected + "\n" +
|
|
102
|
+
"retry: devrites-engine check readiness " + slug + "\n"
|
|
103
|
+
if code != 3 || out != wantOut || errOut != "" {
|
|
104
|
+
t.Fatalf("code=%d stdout=%q stderr=%q, want code=3 stdout=%q stderr empty", code, out, errOut, wantOut)
|
|
43
105
|
}
|
|
44
106
|
}
|
|
45
107
|
|
|
@@ -74,6 +136,47 @@ func TestReadinessUsesStructuralArtifactsOnly(t *testing.T) {
|
|
|
74
136
|
}
|
|
75
137
|
}
|
|
76
138
|
|
|
139
|
+
func TestGateQuestionPolicyCLIContract(t *testing.T) {
|
|
140
|
+
const openQuestion = "## q-1\nstatus: open\ngate: blocking\n"
|
|
141
|
+
cases := []struct {
|
|
142
|
+
name string
|
|
143
|
+
kind string
|
|
144
|
+
current state.Phase
|
|
145
|
+
required state.Phase
|
|
146
|
+
wantCode int
|
|
147
|
+
wantResult string
|
|
148
|
+
wantReason string
|
|
149
|
+
}{
|
|
150
|
+
{name: "frame-open", kind: "readiness", current: state.PhaseFrame, required: state.PhaseFrame, wantCode: 0, wantResult: "pass", wantReason: "DRV-GATE-READINESS-PASSED"},
|
|
151
|
+
{name: "spec-open", kind: "readiness", current: state.PhaseSpec, required: state.PhaseSpec, wantCode: 0, wantResult: "pass", wantReason: "DRV-GATE-READINESS-PASSED"},
|
|
152
|
+
{name: "clarify-open", kind: "readiness", current: state.PhaseClarify, required: state.PhaseClarify, wantCode: 3, wantResult: "blocked (state invariant)", wantReason: "DRV-GATE-READINESS-MISSING"},
|
|
153
|
+
{name: "seal-from-spec", kind: "seal", current: state.PhaseSpec, required: state.PhaseSeal, wantCode: 3, wantResult: "blocked (state invariant)", wantReason: "DRV-GATE-SEAL-MISSING"},
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
for _, tc := range cases {
|
|
157
|
+
t.Run(tc.name, func(t *testing.T) {
|
|
158
|
+
root := filepath.Join(t.TempDir(), ".devrites")
|
|
159
|
+
writeCompleteGateCLIWorkspace(t, root, tc.name, tc.current, tc.required, openQuestion)
|
|
160
|
+
if tc.kind == "seal" {
|
|
161
|
+
binding, err := internalgate.ReadinessBinding(root, tc.name)
|
|
162
|
+
if err != nil {
|
|
163
|
+
t.Fatal(err)
|
|
164
|
+
}
|
|
165
|
+
testutil.AppendFile(t, filepath.Join(root, "work", tc.name, "eng-review.md"), "\n"+binding+"\n")
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
out, errOut, code := runDevrites(t, root, "check", tc.kind, tc.name)
|
|
169
|
+
wantOut := "gate: " + tc.kind + "\nfeature: " + tc.name + "\nphase: " + string(tc.current) + "\nresult: " + tc.wantResult + "\nreason: " + tc.wantReason + "\n"
|
|
170
|
+
if tc.wantCode == 3 {
|
|
171
|
+
wantOut += "invariant: open blocking human question(s) remain in questions.md but state.md is not awaiting_human\nretry: devrites-engine check " + tc.kind + " " + tc.name + "\n"
|
|
172
|
+
}
|
|
173
|
+
if code != tc.wantCode || out != wantOut || errOut != "" {
|
|
174
|
+
t.Fatalf("code=%d stdout=%q stderr=%q, want code=%d stdout=%q stderr empty", code, out, errOut, tc.wantCode, wantOut)
|
|
175
|
+
}
|
|
176
|
+
})
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
77
180
|
func TestSealBlocksWhenSealSectionsMissing(t *testing.T) {
|
|
78
181
|
root := newWorkspace(t)
|
|
79
182
|
out, _, code := runDevrites(t, root, "check", "seal", "auth-tokens")
|
|
@@ -91,19 +194,21 @@ func TestSealPassesWhenComplete(t *testing.T) {
|
|
|
91
194
|
if err := os.Chtimes(source, time.Unix(2_000_000_000, 0), time.Unix(2_000_000_000, 0)); err != nil {
|
|
92
195
|
t.Fatal(err)
|
|
93
196
|
}
|
|
197
|
+
digest, _, err := lib.CandidateIdentity(root, "final")
|
|
198
|
+
if err != nil {
|
|
199
|
+
t.Fatal(err)
|
|
200
|
+
}
|
|
94
201
|
chdirForSeal(t, project)
|
|
95
202
|
|
|
96
203
|
out, errOut, code := runDevrites(t, root, "check", "seal", "final")
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
"result: pass"
|
|
102
|
-
"
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
t.Fatalf("stdout missing %q:\n%s", want, out)
|
|
106
|
-
}
|
|
204
|
+
wantOut := "evidence-fresh: OK: candidate digest " + digest + " matches evidence, review, and seal.\n" +
|
|
205
|
+
"gate: seal\n" +
|
|
206
|
+
"feature: final\n" +
|
|
207
|
+
"phase: seal\n" +
|
|
208
|
+
"result: pass\n" +
|
|
209
|
+
"reason: DRV-GATE-SEAL-PASSED\n"
|
|
210
|
+
if code != 0 || out != wantOut || errOut != "" {
|
|
211
|
+
t.Fatalf("code=%d stdout=%q stderr=%q, want code=0 stdout=%q stderr empty", code, out, errOut, wantOut)
|
|
107
212
|
}
|
|
108
213
|
}
|
|
109
214
|
|
|
@@ -198,6 +303,276 @@ func TestSealTracksCanonicalQuestionTableStatus(t *testing.T) {
|
|
|
198
303
|
}
|
|
199
304
|
}
|
|
200
305
|
|
|
306
|
+
func TestWorkspaceObservationEarlyStateErrorsCLIContract(t *testing.T) {
|
|
307
|
+
for _, test := range []struct {
|
|
308
|
+
name string
|
|
309
|
+
slug string
|
|
310
|
+
mutate func(*testing.T, string)
|
|
311
|
+
wantErr string
|
|
312
|
+
notDisclose []string
|
|
313
|
+
}{
|
|
314
|
+
{
|
|
315
|
+
name: "absent state",
|
|
316
|
+
slug: "absent-state",
|
|
317
|
+
mutate: func(t *testing.T, workspace string) {
|
|
318
|
+
if err := os.Remove(filepath.Join(workspace, "state.md")); err != nil {
|
|
319
|
+
t.Fatal(err)
|
|
320
|
+
}
|
|
321
|
+
},
|
|
322
|
+
wantErr: `devrites: gate readiness: feature "absent-state": state.md is absent; add real content to state.md and retry
|
|
323
|
+
`,
|
|
324
|
+
},
|
|
325
|
+
{
|
|
326
|
+
name: "empty state",
|
|
327
|
+
slug: "empty-state",
|
|
328
|
+
mutate: func(t *testing.T, workspace string) {
|
|
329
|
+
testutil.WriteFile(t, filepath.Join(workspace, "state.md"), "")
|
|
330
|
+
},
|
|
331
|
+
wantErr: `devrites: gate readiness: feature "empty-state": state.md is empty; add real content to state.md and retry
|
|
332
|
+
`,
|
|
333
|
+
},
|
|
334
|
+
{
|
|
335
|
+
name: "malformed state",
|
|
336
|
+
slug: "malformed-state",
|
|
337
|
+
mutate: func(t *testing.T, workspace string) {
|
|
338
|
+
testutil.WriteFile(t, filepath.Join(workspace, "state.md"), "# State\n\nhostile-secret\x00\n")
|
|
339
|
+
},
|
|
340
|
+
wantErr: `devrites: gate readiness: feature "malformed-state": state.md is malformed (malformed_markdown); repair state.md and retry
|
|
341
|
+
`,
|
|
342
|
+
notDisclose: []string{"hostile-secret"},
|
|
343
|
+
},
|
|
344
|
+
{
|
|
345
|
+
name: "unsafe state final symlink",
|
|
346
|
+
slug: "unsafe-state",
|
|
347
|
+
mutate: func(t *testing.T, workspace string) {
|
|
348
|
+
const target = "hostile-state-target.md"
|
|
349
|
+
statePath := filepath.Join(workspace, "state.md")
|
|
350
|
+
if err := os.Remove(statePath); err != nil {
|
|
351
|
+
t.Fatal(err)
|
|
352
|
+
}
|
|
353
|
+
testutil.WriteFile(t, filepath.Join(workspace, target), "- Phase: spec\n- Status: running\n")
|
|
354
|
+
if err := os.Symlink(target, statePath); err != nil {
|
|
355
|
+
t.Fatalf("create final symlink fixture: %v", err)
|
|
356
|
+
}
|
|
357
|
+
},
|
|
358
|
+
wantErr: `devrites: gate readiness: feature "unsafe-state": state.md is unsafe (final_symlink); repair state.md and retry
|
|
359
|
+
`,
|
|
360
|
+
notDisclose: []string{"hostile-state-target.md"},
|
|
361
|
+
},
|
|
362
|
+
{
|
|
363
|
+
name: "state without phase",
|
|
364
|
+
slug: "state-without-phase",
|
|
365
|
+
mutate: func(t *testing.T, workspace string) {
|
|
366
|
+
testutil.WriteFile(t, filepath.Join(workspace, "state.md"), "# State\n\n- Status: running\n")
|
|
367
|
+
},
|
|
368
|
+
wantErr: `devrites: gate readiness: feature "state-without-phase": no phase in state.md ledger; record phase in state.md and retry
|
|
369
|
+
`,
|
|
370
|
+
},
|
|
371
|
+
{
|
|
372
|
+
name: "state with unknown phase",
|
|
373
|
+
slug: "state-with-unknown-phase",
|
|
374
|
+
mutate: func(t *testing.T, workspace string) {
|
|
375
|
+
testutil.WriteFile(t, filepath.Join(workspace, "state.md"), "# State\n\n- Phase: mystery\n- Status: running\n")
|
|
376
|
+
},
|
|
377
|
+
wantErr: `devrites: gate readiness: feature "state-with-unknown-phase": unknown phase "mystery"; record a known phase in state.md and retry
|
|
378
|
+
`,
|
|
379
|
+
},
|
|
380
|
+
} {
|
|
381
|
+
t.Run(test.name, func(t *testing.T) {
|
|
382
|
+
root := filepath.Join(t.TempDir(), ".devrites")
|
|
383
|
+
writeCompleteGateCLIWorkspace(t, root, test.slug, state.PhaseSpec, state.PhaseSpec, "none\n")
|
|
384
|
+
workspace := filepath.Join(root, "work", test.slug)
|
|
385
|
+
test.mutate(t, workspace)
|
|
386
|
+
|
|
387
|
+
out, errOut, code := runDevrites(t, root, "check", "readiness", test.slug)
|
|
388
|
+
if code != 2 || out != "" || errOut != test.wantErr {
|
|
389
|
+
t.Fatalf("code=%d stdout=%q stderr=%q, want code=2 stdout empty stderr=%q", code, out, errOut, test.wantErr)
|
|
390
|
+
}
|
|
391
|
+
for _, secret := range test.notDisclose {
|
|
392
|
+
if strings.Contains(out, secret) || strings.Contains(errOut, secret) {
|
|
393
|
+
t.Errorf("CLI output disclosed hostile fixture content %q: stdout=%q stderr=%q", secret, out, errOut)
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
})
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
func TestWorkspaceObservationGateDiagnosticCLIContract(t *testing.T) {
|
|
401
|
+
root := filepath.Join(t.TempDir(), ".devrites")
|
|
402
|
+
writeCompleteGateCLIWorkspace(t, root, "diagnostic", state.PhaseSpec, state.PhaseSpec, "none\n")
|
|
403
|
+
testutil.WriteFile(t, filepath.Join(root, "work", "diagnostic", "spec.md"), "hostile-secret\x00")
|
|
404
|
+
|
|
405
|
+
out, errOut, code := runDevrites(t, root, "check", "readiness", "diagnostic")
|
|
406
|
+
wantOut := "gate: readiness\n" +
|
|
407
|
+
"feature: diagnostic\n" +
|
|
408
|
+
"phase: spec\n" +
|
|
409
|
+
"result: blocked (missing to leave \"spec\": spec.md)\n" +
|
|
410
|
+
"reason: DRV-GATE-READINESS-MISSING\n" +
|
|
411
|
+
"artifact: spec.md: malformed (malformed_markdown)\n" +
|
|
412
|
+
"next: repair spec.md: replace invalid Markdown with valid Markdown; required artifacts need substantive content\n" +
|
|
413
|
+
"retry: devrites-engine check readiness diagnostic\n"
|
|
414
|
+
if code != 3 || out != wantOut || errOut != "" {
|
|
415
|
+
t.Fatalf("code=%d stdout=%q stderr=%q, want code=3 stdout=%q stderr empty", code, out, errOut, wantOut)
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
func TestWorkspaceObservationStandaloneBindingDiagnosticCLIContract(t *testing.T) {
|
|
420
|
+
root := filepath.Join(t.TempDir(), ".devrites")
|
|
421
|
+
writeCompleteGateCLIWorkspace(t, root, "binding-diagnostic", state.PhaseBuild, state.PhaseBuild, "none\n")
|
|
422
|
+
testutil.WriteFile(t, filepath.Join(root, "work", "binding-diagnostic", "strategy.md"), "hostile-secret\x00")
|
|
423
|
+
|
|
424
|
+
out, errOut, code := runDevrites(t, root, "check", "readiness", "--emit-binding", "binding-diagnostic")
|
|
425
|
+
wantErr := "readiness-binding: BLOCKED: readiness input strategy.md is malformed (malformed_markdown); replace invalid Markdown with valid Markdown\n"
|
|
426
|
+
if code != 3 || out != "" || errOut != wantErr {
|
|
427
|
+
t.Fatalf("code=%d stdout=%q stderr=%q, want code=3 stdout empty stderr=%q", code, out, errOut, wantErr)
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
func TestWorkspaceObservationConstructibleDiagnosticsCLIContract(t *testing.T) {
|
|
432
|
+
for _, test := range []struct {
|
|
433
|
+
name string
|
|
434
|
+
slug string
|
|
435
|
+
mutate func(*testing.T, string)
|
|
436
|
+
diagnostic string
|
|
437
|
+
recovery string
|
|
438
|
+
}{
|
|
439
|
+
{
|
|
440
|
+
name: "final symlink gate",
|
|
441
|
+
slug: "final-symlink",
|
|
442
|
+
mutate: func(t *testing.T, workspace string) {
|
|
443
|
+
path := filepath.Join(workspace, "spec.md")
|
|
444
|
+
if err := os.Remove(path); err != nil {
|
|
445
|
+
t.Fatal(err)
|
|
446
|
+
}
|
|
447
|
+
if err := os.Symlink("brief.md", path); err != nil {
|
|
448
|
+
t.Fatalf("create final symlink fixture: %v", err)
|
|
449
|
+
}
|
|
450
|
+
},
|
|
451
|
+
diagnostic: "artifact: spec.md: unsafe (final_symlink)\n",
|
|
452
|
+
recovery: "next: repair spec.md: replace the symlink with a regular file\n",
|
|
453
|
+
},
|
|
454
|
+
{
|
|
455
|
+
name: "nonregular gate",
|
|
456
|
+
slug: "nonregular",
|
|
457
|
+
mutate: func(t *testing.T, workspace string) {
|
|
458
|
+
path := filepath.Join(workspace, "spec.md")
|
|
459
|
+
if err := os.Remove(path); err != nil {
|
|
460
|
+
t.Fatal(err)
|
|
461
|
+
}
|
|
462
|
+
if err := os.Mkdir(path, 0o755); err != nil {
|
|
463
|
+
t.Fatal(err)
|
|
464
|
+
}
|
|
465
|
+
},
|
|
466
|
+
diagnostic: "artifact: spec.md: unsafe (non_regular)\n",
|
|
467
|
+
recovery: "next: repair spec.md: replace the non-regular entry with a regular file\n",
|
|
468
|
+
},
|
|
469
|
+
} {
|
|
470
|
+
t.Run(test.name, func(t *testing.T) {
|
|
471
|
+
root := filepath.Join(t.TempDir(), ".devrites")
|
|
472
|
+
writeCompleteGateCLIWorkspace(t, root, test.slug, state.PhaseSpec, state.PhaseSpec, "none\n")
|
|
473
|
+
workspace := filepath.Join(root, "work", test.slug)
|
|
474
|
+
test.mutate(t, workspace)
|
|
475
|
+
|
|
476
|
+
out, errOut, code := runDevrites(t, root, "check", "readiness", test.slug)
|
|
477
|
+
wantOut := "gate: readiness\n" +
|
|
478
|
+
"feature: " + test.slug + "\n" +
|
|
479
|
+
"phase: spec\n" +
|
|
480
|
+
"result: blocked (missing to leave \"spec\": spec.md)\n" +
|
|
481
|
+
"reason: DRV-GATE-READINESS-MISSING\n" +
|
|
482
|
+
test.diagnostic +
|
|
483
|
+
test.recovery +
|
|
484
|
+
"retry: devrites-engine check readiness " + test.slug + "\n"
|
|
485
|
+
if code != 3 || out != wantOut || errOut != "" {
|
|
486
|
+
t.Fatalf("code=%d stdout=%q stderr=%q, want code=3 stdout=%q stderr empty", code, out, errOut, wantOut)
|
|
487
|
+
}
|
|
488
|
+
})
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
t.Run("file too large standalone binding", func(t *testing.T) {
|
|
492
|
+
root := filepath.Join(t.TempDir(), ".devrites")
|
|
493
|
+
writeCompleteGateCLIWorkspace(t, root, "binding-too-large", state.PhaseBuild, state.PhaseBuild, "none\n")
|
|
494
|
+
testutil.WriteFile(t, filepath.Join(root, "work", "binding-too-large", "strategy.md"), strings.Repeat("x", (1<<20)+1))
|
|
495
|
+
|
|
496
|
+
out, errOut, code := runDevrites(t, root, "check", "readiness", "--emit-binding", "binding-too-large")
|
|
497
|
+
wantErr := "readiness-binding: BLOCKED: readiness input strategy.md is unsafe (file_too_large); reduce the file to at most 1 MiB\n"
|
|
498
|
+
if code != 3 || out != "" || errOut != wantErr {
|
|
499
|
+
t.Fatalf("code=%d stdout=%q stderr=%q, want code=3 stdout empty stderr=%q", code, out, errOut, wantErr)
|
|
500
|
+
}
|
|
501
|
+
})
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
func TestWorkspaceObservationWholeFailureCLIChannels(t *testing.T) {
|
|
505
|
+
t.Run("workspace invalid gate", func(t *testing.T) {
|
|
506
|
+
root := filepath.Join(t.TempDir(), ".devrites")
|
|
507
|
+
if err := os.MkdirAll(root, 0o755); err != nil {
|
|
508
|
+
t.Fatal(err)
|
|
509
|
+
}
|
|
510
|
+
out, errOut, code := runDevrites(t, root, "check", "readiness", "missing")
|
|
511
|
+
wantErr := "devrites: gate readiness: workspace observation: workspace_invalid: workspace is unavailable; verify the selected logical workspace and canonical workspace override, then retry\n"
|
|
512
|
+
if code != 2 || out != "" || errOut != wantErr {
|
|
513
|
+
t.Fatalf("code=%d stdout=%q stderr=%q, want code=2 stdout empty stderr=%q", code, out, errOut, wantErr)
|
|
514
|
+
}
|
|
515
|
+
})
|
|
516
|
+
|
|
517
|
+
t.Run("workspace invalid binding", func(t *testing.T) {
|
|
518
|
+
root := filepath.Join(t.TempDir(), ".devrites")
|
|
519
|
+
if err := os.MkdirAll(root, 0o755); err != nil {
|
|
520
|
+
t.Fatal(err)
|
|
521
|
+
}
|
|
522
|
+
out, errOut, code := runDevrites(t, root, "check", "readiness", "--emit-binding", "missing")
|
|
523
|
+
wantErr := "readiness-binding: BLOCKED: workspace observation: workspace_invalid: workspace is unavailable; verify the selected logical workspace and canonical workspace override, then retry\n"
|
|
524
|
+
if code != 3 || out != "" || errOut != wantErr {
|
|
525
|
+
t.Fatalf("code=%d stdout=%q stderr=%q, want code=3 stdout empty stderr=%q", code, out, errOut, wantErr)
|
|
526
|
+
}
|
|
527
|
+
})
|
|
528
|
+
|
|
529
|
+
t.Run("aggregate too large gate and binding", func(t *testing.T) {
|
|
530
|
+
root := filepath.Join(t.TempDir(), ".devrites")
|
|
531
|
+
writeCompleteGateCLIWorkspace(t, root, "aggregate", state.PhaseBuild, state.PhaseBuild, "none\n")
|
|
532
|
+
for i, name := range []string{"state.md", "brief.md", "spec.md", "decisions.md", "assumptions.md", "questions.md", "decision-coverage.md", "architecture.md", "plan.md"} {
|
|
533
|
+
prefix := "retained content\n"
|
|
534
|
+
if i == 0 {
|
|
535
|
+
prefix = "- Phase: build\n"
|
|
536
|
+
}
|
|
537
|
+
body := prefix + strings.Repeat("x", (1<<20)-len(prefix))
|
|
538
|
+
testutil.WriteFile(t, filepath.Join(root, "work", "aggregate", name), body)
|
|
539
|
+
}
|
|
540
|
+
wantFailure := "workspace observation: aggregate_too_large: retained content exceeds the 8 MiB aggregate limit; reduce retained Markdown below 8 MiB, then retry"
|
|
541
|
+
out, errOut, code := runDevrites(t, root, "check", "readiness", "aggregate")
|
|
542
|
+
if code != 2 || out != "" || errOut != "devrites: gate readiness: "+wantFailure+"\n" {
|
|
543
|
+
t.Fatalf("gate code=%d stdout=%q stderr=%q", code, out, errOut)
|
|
544
|
+
}
|
|
545
|
+
out, errOut, code = runDevrites(t, root, "check", "readiness", "--emit-binding", "aggregate")
|
|
546
|
+
if code != 3 || out != "" || errOut != "readiness-binding: BLOCKED: "+wantFailure+"\n" {
|
|
547
|
+
t.Fatalf("binding code=%d stdout=%q stderr=%q", code, out, errOut)
|
|
548
|
+
}
|
|
549
|
+
})
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
func writeCompleteGateCLIWorkspace(t *testing.T, root, slug string, current, required state.Phase, questions string) {
|
|
553
|
+
t.Helper()
|
|
554
|
+
policy, ok := state.PolicyFor(required)
|
|
555
|
+
if !ok {
|
|
556
|
+
t.Fatalf("PolicyFor(%q) returned unknown", required)
|
|
557
|
+
}
|
|
558
|
+
questionsRequired := false
|
|
559
|
+
for _, artifact := range policy.RequiredArtifacts {
|
|
560
|
+
name := string(artifact)
|
|
561
|
+
content := "# " + name + "\n\nreal\n"
|
|
562
|
+
switch name {
|
|
563
|
+
case "state.md":
|
|
564
|
+
content = "- Phase: " + string(current) + "\n- Status: running\n"
|
|
565
|
+
case "questions.md":
|
|
566
|
+
questionsRequired = true
|
|
567
|
+
content = questions
|
|
568
|
+
}
|
|
569
|
+
testutil.WriteFile(t, filepath.Join(root, "work", slug, name), content)
|
|
570
|
+
}
|
|
571
|
+
if !questionsRequired {
|
|
572
|
+
testutil.WriteFile(t, filepath.Join(root, "work", slug, "questions.md"), questions)
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
201
576
|
func newFinalSealRepo(t *testing.T) (string, string) {
|
|
202
577
|
t.Helper()
|
|
203
578
|
requireGit(t)
|