devrites 5.12.0 → 5.13.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 +5 -3
- package/docs/architecture.md +2 -2
- package/docs/command-map.md +3 -1
- package/docs/engine/commands.md +21 -0
- package/docs/flow.md +2 -1
- package/docs/skills.md +13 -4
- package/engine/help.go +4 -1
- package/engine/help_test.go +2 -0
- package/engine/internal/overhaul/admit/admit_test.go +170 -0
- package/engine/internal/overhaul/admit/run.go +416 -0
- package/engine/internal/overhaul/bench/run.go +248 -0
- package/engine/internal/overhaul/bench/run_test.go +139 -0
- package/engine/internal/overhaul/overhaul.go +64 -0
- package/engine/internal/overhaul/ovio/ovio.go +158 -0
- package/engine/internal/overhaul/records/check.go +834 -0
- package/engine/internal/overhaul/records/records_test.go +577 -0
- package/engine/internal/overhaul/records/run.go +392 -0
- package/engine/internal/overhaul/render/run.go +776 -0
- package/engine/internal/overhaul/render/run_test.go +293 -0
- package/engine/internal/overhaul/score/run.go +600 -0
- package/engine/internal/overhaul/score/run_test.go +405 -0
- package/engine/internal/overhaul/snapshot/run.go +563 -0
- package/engine/internal/overhaul/snapshot/run_test.go +360 -0
- package/engine/main.go +4 -0
- package/engine/root_routing_test.go +2 -1
- package/pack/.claude/skills/overhaul/SKILL.md +201 -0
- package/pack/.claude/skills/overhaul/references/audit-domains.md +263 -0
- package/pack/.claude/skills/overhaul/references/lifecycle.md +308 -0
- package/pack/.claude/skills/overhaul/references/orchestration.md +264 -0
- package/pack/.claude/skills/overhaul/references/records.md +137 -0
- package/pack/.claude/skills/overhaul/references/repair-and-measurement.md +207 -0
- package/pack/.claude/skills/overhaul/references/review-and-report.md +137 -0
- package/pack/.claude/skills/overhaul/references/scope-and-safety.md +191 -0
- package/pack/.claude/skills/overhaul/references/scoring.md +222 -0
- package/pack/.claude/skills/overhaul/references/stack-profiles.md +264 -0
- package/pack/.claude/skills/overhaul/roles/backend-engineer.md +61 -0
- package/pack/.claude/skills/overhaul/roles/boundary.md +56 -0
- package/pack/.claude/skills/overhaul/roles/craft.md +77 -0
- package/pack/.claude/skills/overhaul/roles/frontend-engineer.md +63 -0
- package/pack/.claude/skills/overhaul/roles/implementer.md +59 -0
- package/pack/.claude/skills/overhaul/roles/recon.md +60 -0
- package/pack/.claude/skills/overhaul/roles/specialist.md +56 -0
- package/pack/.claude/skills/overhaul/roles/verifier.md +60 -0
- package/pack/generated/claude/skills/overhaul/SKILL.md +201 -0
- package/pack/generated/claude/skills/overhaul/references/audit-domains.md +263 -0
- package/pack/generated/claude/skills/overhaul/references/lifecycle.md +308 -0
- package/pack/generated/claude/skills/overhaul/references/orchestration.md +264 -0
- package/pack/generated/claude/skills/overhaul/references/records.md +137 -0
- package/pack/generated/claude/skills/overhaul/references/repair-and-measurement.md +207 -0
- package/pack/generated/claude/skills/overhaul/references/review-and-report.md +137 -0
- package/pack/generated/claude/skills/overhaul/references/scope-and-safety.md +191 -0
- package/pack/generated/claude/skills/overhaul/references/scoring.md +222 -0
- package/pack/generated/claude/skills/overhaul/references/stack-profiles.md +264 -0
- package/pack/generated/claude/skills/overhaul/roles/backend-engineer.md +61 -0
- package/pack/generated/claude/skills/overhaul/roles/boundary.md +56 -0
- package/pack/generated/claude/skills/overhaul/roles/craft.md +77 -0
- package/pack/generated/claude/skills/overhaul/roles/frontend-engineer.md +63 -0
- package/pack/generated/claude/skills/overhaul/roles/implementer.md +59 -0
- package/pack/generated/claude/skills/overhaul/roles/recon.md +60 -0
- package/pack/generated/claude/skills/overhaul/roles/specialist.md +56 -0
- package/pack/generated/claude/skills/overhaul/roles/verifier.md +60 -0
- package/pack/generated/codex/skills/overhaul/SKILL.md +201 -0
- package/pack/generated/codex/skills/overhaul/agents/openai.yaml +2 -0
- package/pack/generated/codex/skills/overhaul/references/audit-domains.md +263 -0
- package/pack/generated/codex/skills/overhaul/references/lifecycle.md +308 -0
- package/pack/generated/codex/skills/overhaul/references/orchestration.md +264 -0
- package/pack/generated/codex/skills/overhaul/references/records.md +137 -0
- package/pack/generated/codex/skills/overhaul/references/repair-and-measurement.md +207 -0
- package/pack/generated/codex/skills/overhaul/references/review-and-report.md +137 -0
- package/pack/generated/codex/skills/overhaul/references/scope-and-safety.md +191 -0
- package/pack/generated/codex/skills/overhaul/references/scoring.md +222 -0
- package/pack/generated/codex/skills/overhaul/references/stack-profiles.md +264 -0
- package/pack/generated/codex/skills/overhaul/roles/backend-engineer.md +61 -0
- package/pack/generated/codex/skills/overhaul/roles/boundary.md +56 -0
- package/pack/generated/codex/skills/overhaul/roles/craft.md +77 -0
- package/pack/generated/codex/skills/overhaul/roles/frontend-engineer.md +63 -0
- package/pack/generated/codex/skills/overhaul/roles/implementer.md +59 -0
- package/pack/generated/codex/skills/overhaul/roles/recon.md +60 -0
- package/pack/generated/codex/skills/overhaul/roles/specialist.md +56 -0
- package/pack/generated/codex/skills/overhaul/roles/verifier.md +60 -0
- package/pack/generated/devin/skills/overhaul/SKILL.md +201 -0
- package/pack/generated/devin/skills/overhaul/references/audit-domains.md +263 -0
- package/pack/generated/devin/skills/overhaul/references/lifecycle.md +308 -0
- package/pack/generated/devin/skills/overhaul/references/orchestration.md +264 -0
- package/pack/generated/devin/skills/overhaul/references/records.md +137 -0
- package/pack/generated/devin/skills/overhaul/references/repair-and-measurement.md +207 -0
- package/pack/generated/devin/skills/overhaul/references/review-and-report.md +137 -0
- package/pack/generated/devin/skills/overhaul/references/scope-and-safety.md +191 -0
- package/pack/generated/devin/skills/overhaul/references/scoring.md +222 -0
- package/pack/generated/devin/skills/overhaul/references/stack-profiles.md +264 -0
- package/pack/generated/devin/skills/overhaul/roles/backend-engineer.md +61 -0
- package/pack/generated/devin/skills/overhaul/roles/boundary.md +56 -0
- package/pack/generated/devin/skills/overhaul/roles/craft.md +77 -0
- package/pack/generated/devin/skills/overhaul/roles/frontend-engineer.md +63 -0
- package/pack/generated/devin/skills/overhaul/roles/implementer.md +59 -0
- package/pack/generated/devin/skills/overhaul/roles/recon.md +60 -0
- package/pack/generated/devin/skills/overhaul/roles/specialist.md +56 -0
- package/pack/generated/devin/skills/overhaul/roles/verifier.md +60 -0
- package/pack/generated/omp/skills/overhaul/SKILL.md +201 -0
- package/pack/generated/omp/skills/overhaul/references/audit-domains.md +263 -0
- package/pack/generated/omp/skills/overhaul/references/lifecycle.md +308 -0
- package/pack/generated/omp/skills/overhaul/references/orchestration.md +264 -0
- package/pack/generated/omp/skills/overhaul/references/records.md +137 -0
- package/pack/generated/omp/skills/overhaul/references/repair-and-measurement.md +207 -0
- package/pack/generated/omp/skills/overhaul/references/review-and-report.md +137 -0
- package/pack/generated/omp/skills/overhaul/references/scope-and-safety.md +191 -0
- package/pack/generated/omp/skills/overhaul/references/scoring.md +222 -0
- package/pack/generated/omp/skills/overhaul/references/stack-profiles.md +264 -0
- package/pack/generated/omp/skills/overhaul/roles/backend-engineer.md +61 -0
- package/pack/generated/omp/skills/overhaul/roles/boundary.md +56 -0
- package/pack/generated/omp/skills/overhaul/roles/craft.md +77 -0
- package/pack/generated/omp/skills/overhaul/roles/frontend-engineer.md +63 -0
- package/pack/generated/omp/skills/overhaul/roles/implementer.md +59 -0
- package/pack/generated/omp/skills/overhaul/roles/recon.md +60 -0
- package/pack/generated/omp/skills/overhaul/roles/specialist.md +56 -0
- package/pack/generated/omp/skills/overhaul/roles/verifier.md +60 -0
- package/pack/generated/pi/skills/overhaul/SKILL.md +201 -0
- package/pack/generated/pi/skills/overhaul/references/audit-domains.md +263 -0
- package/pack/generated/pi/skills/overhaul/references/lifecycle.md +308 -0
- package/pack/generated/pi/skills/overhaul/references/orchestration.md +264 -0
- package/pack/generated/pi/skills/overhaul/references/records.md +137 -0
- package/pack/generated/pi/skills/overhaul/references/repair-and-measurement.md +207 -0
- package/pack/generated/pi/skills/overhaul/references/review-and-report.md +137 -0
- package/pack/generated/pi/skills/overhaul/references/scope-and-safety.md +191 -0
- package/pack/generated/pi/skills/overhaul/references/scoring.md +222 -0
- package/pack/generated/pi/skills/overhaul/references/stack-profiles.md +264 -0
- package/pack/generated/pi/skills/overhaul/roles/backend-engineer.md +61 -0
- package/pack/generated/pi/skills/overhaul/roles/boundary.md +56 -0
- package/pack/generated/pi/skills/overhaul/roles/craft.md +77 -0
- package/pack/generated/pi/skills/overhaul/roles/frontend-engineer.md +63 -0
- package/pack/generated/pi/skills/overhaul/roles/implementer.md +59 -0
- package/pack/generated/pi/skills/overhaul/roles/recon.md +60 -0
- package/pack/generated/pi/skills/overhaul/roles/specialist.md +56 -0
- package/pack/generated/pi/skills/overhaul/roles/verifier.md +60 -0
- package/package.json +1 -1
|
@@ -0,0 +1,577 @@
|
|
|
1
|
+
package records
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"bytes"
|
|
5
|
+
"encoding/json"
|
|
6
|
+
"os"
|
|
7
|
+
"path/filepath"
|
|
8
|
+
"strings"
|
|
9
|
+
"testing"
|
|
10
|
+
|
|
11
|
+
"github.com/devrites/devrites/internal/overhaul/ovio"
|
|
12
|
+
"github.com/devrites/devrites/internal/overhaul/render"
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
type J = map[string]any
|
|
16
|
+
type L = []any
|
|
17
|
+
|
|
18
|
+
type fixture struct {
|
|
19
|
+
t *testing.T
|
|
20
|
+
repo, run string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
func write(t *testing.T, path string, v any) {
|
|
24
|
+
t.Helper()
|
|
25
|
+
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
|
26
|
+
t.Fatal(err)
|
|
27
|
+
}
|
|
28
|
+
var b []byte
|
|
29
|
+
switch x := v.(type) {
|
|
30
|
+
case string:
|
|
31
|
+
b = []byte(x)
|
|
32
|
+
default:
|
|
33
|
+
var err error
|
|
34
|
+
if b, err = json.MarshalIndent(v, "", " "); err != nil {
|
|
35
|
+
t.Fatal(err)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if err := os.WriteFile(path, b, 0o644); err != nil {
|
|
39
|
+
t.Fatal(err)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
func digest(t *testing.T, path string) string {
|
|
44
|
+
t.Helper()
|
|
45
|
+
d, err := ovio.SHA256File(path)
|
|
46
|
+
if err != nil {
|
|
47
|
+
t.Fatal(err)
|
|
48
|
+
}
|
|
49
|
+
return d
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
func task(id string, deps L, path string) J {
|
|
53
|
+
return J{"id": id, "deps": deps, "allowed_paths": L{path}, "oracle": "test passes",
|
|
54
|
+
"writer_role": "implementer", "verifier_role": "verifier", "controls": L{"C-1"}}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
func newFixture(t *testing.T) *fixture {
|
|
58
|
+
dir := t.TempDir()
|
|
59
|
+
f := &fixture{t: t, repo: filepath.Join(dir, "repo"), run: filepath.Join(dir, "run")}
|
|
60
|
+
write(t, filepath.Join(f.repo, "src/a.py"), "print('a')\n")
|
|
61
|
+
write(t, filepath.Join(f.run, "revisions/rubric-r1.json"), J{"schema": "overhaul.rubric/1", "rev": 1, "controls": L{J{"id": "C-1"}}})
|
|
62
|
+
f.writePlan(L{task("T-1", L{}, "src/a.py"), task("T-2", L{"T-1"}, "src/b.py")})
|
|
63
|
+
write(t, filepath.Join(f.run, "evidence/red.log"), "red\n")
|
|
64
|
+
write(t, filepath.Join(f.run, "receipts/R-1.json"), J{})
|
|
65
|
+
return f
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
func (f *fixture) writePlan(tasks L) {
|
|
69
|
+
write(f.t, filepath.Join(f.run, "revisions/plan-r1.json"), J{"schema": "overhaul.plan/1", "rev": 1,
|
|
70
|
+
"rubric": J{"file": "revisions/rubric-r1.json", "sha256": digest(f.t, filepath.Join(f.run, "revisions/rubric-r1.json"))},
|
|
71
|
+
"tasks": tasks})
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// records returns a fresh, valid record set with digests of the current files.
|
|
75
|
+
func (f *fixture) records() map[string]J {
|
|
76
|
+
return map[string]J{
|
|
77
|
+
"run.json": {"schema": "overhaul.run/1", "run_id": "run-1", "repo_root": f.repo, "mode": "full",
|
|
78
|
+
"assessment_only": false, "phase": "REMEDIATING", "readiness_verdict": "NOT_ASSESSED",
|
|
79
|
+
"execution_outcome": "RUNNING", "revisions": J{"plan": J{"rev": 1, "file": "revisions/plan-r1.json"}}},
|
|
80
|
+
"coverage.json": {"schema": "overhaul.coverage/1", "files": L{J{"path": "src/a.py", "eligible": true}}},
|
|
81
|
+
"findings.json": {"schema": "overhaul.findings/1", "findings": L{J{"id": "F-1", "fingerprint": "fp-1",
|
|
82
|
+
"kind": "defect", "scope_origin": "pre-existing", "status": "confirmed", "severity": "high",
|
|
83
|
+
"evidence": L{"E-1"}, "locations": L{J{"path": "src/a.py", "start": 1, "end": 1},
|
|
84
|
+
J{"path": "mobile-app:src/api.ts", "external": true}},
|
|
85
|
+
"history": L{J{"status": "candidate"}, J{"status": "confirmed"}}}}},
|
|
86
|
+
"evidence.json": {"schema": "overhaul.evidence/1", "items": L{J{"id": "E-1", "path": "evidence/red.log",
|
|
87
|
+
"sha256": digest(f.t, filepath.Join(f.run, "evidence/red.log"))}}},
|
|
88
|
+
"dispatch.json": {"schema": "overhaul.dispatch/1", "attempts": L{J{"task_id": "T-1", "attempt_id": "A1",
|
|
89
|
+
"role": "implementer", "status": "admitted", "authorized_by": "AP-1", "receipt": "receipts/R-1.json",
|
|
90
|
+
"changed_paths": L{"src/a.py"}}}},
|
|
91
|
+
"approval.json": {"schema": "overhaul.approval/1", "events": L{J{"id": "AP-1", "kind": "approve",
|
|
92
|
+
"status": "active", "tasks": L{"T-1"}, "plan_rev": 1, "quote": "yes, fix T-1", "channel": "conversation",
|
|
93
|
+
"plan_digest": digest(f.t, filepath.Join(f.run, "revisions/plan-r1.json"))}}},
|
|
94
|
+
"cycles.json": {"schema": "overhaul.cycles/1", "cycles": L{J{"n": 1}}},
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
func (f *fixture) cmd(args ...string) (int, string, string) {
|
|
99
|
+
var out, errb bytes.Buffer
|
|
100
|
+
code := Run(args, &out, &errb)
|
|
101
|
+
return code, out.String(), errb.String()
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// publish stages, writes recs (plus extra files relative to the stage) and publishes.
|
|
105
|
+
func (f *fixture) publish(recs map[string]J, extra map[string]string) (int, string) {
|
|
106
|
+
f.t.Helper()
|
|
107
|
+
code, out, errs := f.cmd("stage", f.run)
|
|
108
|
+
if code != 0 {
|
|
109
|
+
f.t.Fatalf("stage: %d %s", code, errs)
|
|
110
|
+
}
|
|
111
|
+
tmp := strings.TrimSpace(out)
|
|
112
|
+
for name, doc := range recs {
|
|
113
|
+
write(f.t, filepath.Join(tmp, name), doc)
|
|
114
|
+
}
|
|
115
|
+
for name, body := range extra {
|
|
116
|
+
write(f.t, filepath.Join(tmp, name), body)
|
|
117
|
+
}
|
|
118
|
+
code, out, errs = f.cmd("publish", f.run)
|
|
119
|
+
return code, out + errs
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
func (f *fixture) mustPublish(recs map[string]J) {
|
|
123
|
+
f.t.Helper()
|
|
124
|
+
if code, out := f.publish(recs, nil); code != 0 {
|
|
125
|
+
f.t.Fatalf("publish failed: %d\n%s", code, out)
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
func finding(r map[string]J) J { return r["findings.json"]["findings"].(L)[0].(J) }
|
|
130
|
+
func approval(r map[string]J) J { return r["approval.json"]["events"].(L)[0].(J) }
|
|
131
|
+
func attempts(r map[string]J) L { return r["dispatch.json"]["attempts"].(L) }
|
|
132
|
+
|
|
133
|
+
func addAttempt(r map[string]J, a J) {
|
|
134
|
+
r["dispatch.json"]["attempts"] = append(attempts(r), a)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
func TestPublishThenValidate(t *testing.T) {
|
|
138
|
+
f := newFixture(t)
|
|
139
|
+
code, out := f.publish(f.records(), nil)
|
|
140
|
+
if code != 0 || !strings.Contains(out, "published g0001") {
|
|
141
|
+
t.Fatalf("publish: %d %s", code, out)
|
|
142
|
+
}
|
|
143
|
+
if code, out, _ := f.cmd("validate", f.run); code != 0 || strings.TrimSpace(out) != "valid g0001" {
|
|
144
|
+
t.Fatalf("validate: %d %s", code, out)
|
|
145
|
+
}
|
|
146
|
+
f.mustPublish(f.records())
|
|
147
|
+
m, err := ovio.LoadObject(filepath.Join(f.run, "g0002/manifest.json"))
|
|
148
|
+
if err != nil {
|
|
149
|
+
t.Fatal(err)
|
|
150
|
+
}
|
|
151
|
+
_, hasRun := ovio.Obj(m["files"])["run.json"]
|
|
152
|
+
_, hasPlan := ovio.Obj(m["revisions"])["revisions/plan-r1.json"]
|
|
153
|
+
if m["previous"] != "g0001" || !hasRun || !hasPlan {
|
|
154
|
+
t.Fatalf("manifest: %v", m)
|
|
155
|
+
}
|
|
156
|
+
if code, out, _ := f.cmd("validate", f.run); code != 0 || strings.TrimSpace(out) != "valid g0002" {
|
|
157
|
+
t.Fatalf("validate g0002: %d %s", code, out)
|
|
158
|
+
}
|
|
159
|
+
if code, out, _ := f.cmd("digest", filepath.Join(f.run, "evidence/red.log")); code != 0 ||
|
|
160
|
+
strings.TrimSpace(out) != digest(t, filepath.Join(f.run, "evidence/red.log")) {
|
|
161
|
+
t.Fatalf("digest: %d %s", code, out)
|
|
162
|
+
}
|
|
163
|
+
if code, _, _ := f.cmd("bogus"); code != 2 {
|
|
164
|
+
t.Fatalf("usage exit = %d", code)
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
func TestSingleGenerationRules(t *testing.T) {
|
|
169
|
+
verifiedHistory := L{J{"status": "candidate"}, J{"status": "confirmed"}, J{"status": "approved-for-fix"},
|
|
170
|
+
J{"status": "in-progress"}, J{"status": "fixed-unverified"}, J{"status": "verified"}}
|
|
171
|
+
verifier := J{"task_id": "T-1", "attempt_id": "V1", "role": "verifier", "status": "admitted", "receipt": "receipts/R-1.json"}
|
|
172
|
+
cases := []struct {
|
|
173
|
+
name string
|
|
174
|
+
mutate func(f *fixture, r map[string]J)
|
|
175
|
+
want string // "" means the publish must succeed
|
|
176
|
+
}{
|
|
177
|
+
{"approval without quote", func(f *fixture, r map[string]J) { delete(approval(r), "quote") }, "needs the user's verbatim decision"},
|
|
178
|
+
{"approval untrusted channel", func(f *fixture, r map[string]J) { approval(r)["channel"] = "email" }, "trusted channel"},
|
|
179
|
+
{"accept-degradation without quote", func(f *fixture, r map[string]J) {
|
|
180
|
+
r["approval.json"]["events"] = append(r["approval.json"]["events"].(L), J{"id": "D-1", "kind": "accept-degradation", "channel": "conversation"})
|
|
181
|
+
}, "approval D-1: needs the user's verbatim decision"},
|
|
182
|
+
{"approval digest mismatch", func(f *fixture, r map[string]J) { approval(r)["plan_digest"] = strings.Repeat("0", 64) }, "plan digest does not match"},
|
|
183
|
+
{"subset approval missing prerequisite", func(f *fixture, r map[string]J) { approval(r)["tasks"] = L{"T-2"} }, "miss prerequisites ['T-1']"},
|
|
184
|
+
{"writer for unapproved task", func(f *fixture, r map[string]J) {
|
|
185
|
+
addAttempt(r, J{"task_id": "T-2", "attempt_id": "A1", "role": "implementer", "status": "running", "authorized_by": "AP-1"})
|
|
186
|
+
}, "attempt T-2#A1: writer task is not in the active approval"},
|
|
187
|
+
{"approval active after stop", func(f *fixture, r map[string]J) {
|
|
188
|
+
r["approval.json"]["events"] = append(r["approval.json"]["events"].(L), J{"id": "S-1", "kind": "stop"})
|
|
189
|
+
}, "still active after a later stop/revoke"},
|
|
190
|
+
{"live writer after stop", func(f *fixture, r map[string]J) {
|
|
191
|
+
approval(r)["status"] = "stopped"
|
|
192
|
+
r["approval.json"]["events"] = append(r["approval.json"]["events"].(L), J{"id": "S-1", "kind": "stop"})
|
|
193
|
+
addAttempt(r, J{"task_id": "T-1", "attempt_id": "A2", "role": "implementer", "status": "running", "authorized_by": "AP-1"})
|
|
194
|
+
}, "attempt T-1#A2: writer task is not in the active approval"},
|
|
195
|
+
{"assessment-only with approval", func(f *fixture, r map[string]J) {
|
|
196
|
+
r["run.json"]["assessment_only"] = true
|
|
197
|
+
r["dispatch.json"]["attempts"] = L{}
|
|
198
|
+
}, "assessment-only run carries an active approval"},
|
|
199
|
+
{"assessment-only with writer", func(f *fixture, r map[string]J) {
|
|
200
|
+
r["run.json"]["assessment_only"] = true
|
|
201
|
+
approval(r)["status"] = "revoked"
|
|
202
|
+
}, "writer dispatched in an assessment-only run"},
|
|
203
|
+
{"live writers case alias", func(f *fixture, r map[string]J) {
|
|
204
|
+
addAttempt(r, J{"task_id": "T-1", "attempt_id": "A2", "role": "implementer", "status": "running", "authorized_by": "AP-1", "write_paths": L{"src/a.py"}})
|
|
205
|
+
addAttempt(r, J{"task_id": "T-1", "attempt_id": "A3", "role": "implementer", "status": "unknown", "authorized_by": "AP-1", "write_paths": L{"SRC/A.PY"}})
|
|
206
|
+
}, "single writer: T-1#A2 and T-1#A3 both hold SRC/A.PY"},
|
|
207
|
+
{"live writers symlink alias", func(f *fixture, r map[string]J) {
|
|
208
|
+
if err := os.Symlink("a.py", filepath.Join(f.repo, "src/link.py")); err != nil {
|
|
209
|
+
f.t.Fatal(err)
|
|
210
|
+
}
|
|
211
|
+
addAttempt(r, J{"task_id": "T-1", "attempt_id": "A2", "role": "implementer", "status": "running", "authorized_by": "AP-1", "write_paths": L{"src/a.py"}})
|
|
212
|
+
addAttempt(r, J{"task_id": "T-1", "attempt_id": "A3", "role": "implementer", "status": "dispatched", "authorized_by": "AP-1", "write_paths": L{"src/link.py"}})
|
|
213
|
+
}, "single writer: T-1#A2 and T-1#A3 both hold src/link.py"},
|
|
214
|
+
{"settled writers may share a path", func(f *fixture, r map[string]J) {
|
|
215
|
+
addAttempt(r, J{"task_id": "T-1", "attempt_id": "A2", "role": "implementer", "status": "running", "authorized_by": "AP-1", "write_paths": L{"src/a.py"}})
|
|
216
|
+
addAttempt(r, J{"task_id": "T-1", "attempt_id": "A3", "role": "implementer", "status": "cancelled", "authorized_by": "AP-1", "write_paths": L{"src/a.py"}})
|
|
217
|
+
}, ""},
|
|
218
|
+
{"allowed path escape", func(f *fixture, r map[string]J) {
|
|
219
|
+
f.writePlan(L{task("T-1", L{}, "../outside.py"), task("T-2", L{"T-1"}, "src/b.py")})
|
|
220
|
+
approval(r)["plan_digest"] = digest(f.t, filepath.Join(f.run, "revisions/plan-r1.json"))
|
|
221
|
+
}, "allowed path '../outside.py' not normalized or escapes the repository"},
|
|
222
|
+
{"dependency cycle", func(f *fixture, r map[string]J) {
|
|
223
|
+
f.writePlan(L{task("T-1", L{"T-2"}, "src/a.py"), task("T-2", L{"T-1"}, "src/b.py")})
|
|
224
|
+
approval(r)["plan_digest"] = digest(f.t, filepath.Join(f.run, "revisions/plan-r1.json"))
|
|
225
|
+
approval(r)["tasks"] = L{"T-1", "T-2"}
|
|
226
|
+
}, "task dependencies contain a cycle"},
|
|
227
|
+
{"bracket path accepted", func(f *fixture, r map[string]J) {
|
|
228
|
+
f.writePlan(L{task("T-1", L{}, "src/a.py"), task("T-2", L{"T-1"}, "app/blog/[slug]/page.tsx")})
|
|
229
|
+
approval(r)["plan_digest"] = digest(f.t, filepath.Join(f.run, "revisions/plan-r1.json"))
|
|
230
|
+
finding(r)["locations"] = L{J{"path": "app/blog/[slug]/page.tsx", "start": 1}}
|
|
231
|
+
}, ""},
|
|
232
|
+
{"duplicate fingerprint", func(f *fixture, r map[string]J) {
|
|
233
|
+
r["findings.json"]["findings"] = append(r["findings.json"]["findings"].(L), J{"id": "F-2", "fingerprint": "fp-1",
|
|
234
|
+
"kind": "defect", "scope_origin": "introduced", "status": "candidate", "history": L{J{"status": "candidate"}}})
|
|
235
|
+
}, "finding fingerprint: duplicate id 'fp-1'"},
|
|
236
|
+
{"needs-validation without blockers", func(f *fixture, r map[string]J) {
|
|
237
|
+
r["findings.json"]["findings"] = append(r["findings.json"]["findings"].(L), J{"id": "F-2", "fingerprint": "fp-2",
|
|
238
|
+
"kind": "defect", "scope_origin": "introduced", "status": "needs-validation",
|
|
239
|
+
"history": L{J{"status": "candidate"}, J{"status": "needs-validation"}}})
|
|
240
|
+
}, "finding F-2: needs-validation must name the missing fact (blockers)"},
|
|
241
|
+
{"host path location", func(f *fixture, r map[string]J) {
|
|
242
|
+
finding(r)["locations"] = L{J{"path": "/Users/me/app/a.py", "start": 1}}
|
|
243
|
+
}, "location '/Users/me/app/a.py' is not a repository-relative path or label"},
|
|
244
|
+
{"external host path location", func(f *fixture, r map[string]J) {
|
|
245
|
+
finding(r)["locations"] = L{J{"path": "~/app/a.py", "external": true}}
|
|
246
|
+
}, "is not a repository-relative path or label"},
|
|
247
|
+
{"directory location", func(f *fixture, r map[string]J) {
|
|
248
|
+
finding(r)["locations"] = L{J{"path": "src"}}
|
|
249
|
+
}, "location 'src' is not a repository-relative path or label"},
|
|
250
|
+
{"illegal transition", func(f *fixture, r map[string]J) {
|
|
251
|
+
finding(r)["history"] = L{J{"status": "candidate"}, J{"status": "verified"}, J{"status": "confirmed"}}
|
|
252
|
+
}, "illegal transition candidate -> verified"},
|
|
253
|
+
{"severity on candidate", func(f *fixture, r map[string]J) {
|
|
254
|
+
finding(r)["status"] = "candidate"
|
|
255
|
+
finding(r)["history"] = L{J{"status": "candidate"}}
|
|
256
|
+
}, "severity only for established impact"},
|
|
257
|
+
{"history must start at candidate", func(f *fixture, r map[string]J) {
|
|
258
|
+
finding(r)["history"] = L{J{"status": "confirmed"}}
|
|
259
|
+
}, "must start at candidate"},
|
|
260
|
+
{"same-status entry with reason", func(f *fixture, r map[string]J) {
|
|
261
|
+
finding(r)["history"] = L{J{"status": "candidate"}, J{"status": "confirmed"}, J{"status": "confirmed", "reason": "severity re-assessed"}}
|
|
262
|
+
}, ""},
|
|
263
|
+
{"same-status entry without reason", func(f *fixture, r map[string]J) {
|
|
264
|
+
finding(r)["history"] = L{J{"status": "candidate"}, J{"status": "confirmed"}, J{"status": "confirmed"}}
|
|
265
|
+
}, "illegal transition confirmed -> confirmed"},
|
|
266
|
+
{"evidence sha256 mandatory", func(f *fixture, r map[string]J) {
|
|
267
|
+
delete(r["evidence.json"]["items"].(L)[0].(J), "sha256")
|
|
268
|
+
}, "evidence E-1: sha256 missing"},
|
|
269
|
+
{"evidence outside run dir", func(f *fixture, r map[string]J) {
|
|
270
|
+
r["evidence.json"]["items"].(L)[0].(J)["path"] = "../repo/src/a.py"
|
|
271
|
+
}, "evidence E-1: path missing or outside run dir"},
|
|
272
|
+
{"verified_by without verifier", func(f *fixture, r map[string]J) {
|
|
273
|
+
finding(r)["status"], finding(r)["history"], finding(r)["verified_by"] = "verified", verifiedHistory, "T-1#V1"
|
|
274
|
+
}, "verified_by must name an admitted verifier attempt"},
|
|
275
|
+
{"verified_by names attempt", func(f *fixture, r map[string]J) {
|
|
276
|
+
finding(r)["status"], finding(r)["history"], finding(r)["verified_by"] = "verified", verifiedHistory, "T-1#V1"
|
|
277
|
+
addAttempt(r, verifier)
|
|
278
|
+
}, ""},
|
|
279
|
+
{"verified_by names receipt", func(f *fixture, r map[string]J) {
|
|
280
|
+
finding(r)["status"], finding(r)["history"], finding(r)["verified_by"] = "verified", verifiedHistory, "receipts/R-1.json"
|
|
281
|
+
addAttempt(r, verifier)
|
|
282
|
+
}, ""},
|
|
283
|
+
{"plan file must match rev", func(f *fixture, r map[string]J) {
|
|
284
|
+
r["run.json"]["revisions"] = J{"plan": J{"rev": 1, "file": "revisions/rubric-r1.json"}}
|
|
285
|
+
}, "must be revisions/plan-r<rev>.json"},
|
|
286
|
+
{"approvals need revisions.plan", func(f *fixture, r map[string]J) {
|
|
287
|
+
r["run.json"]["revisions"] = J{}
|
|
288
|
+
r["dispatch.json"]["attempts"] = L{}
|
|
289
|
+
}, "approvals and writers need revisions.plan"},
|
|
290
|
+
{"writers need revisions.plan", func(f *fixture, r map[string]J) {
|
|
291
|
+
r["run.json"]["revisions"] = J{}
|
|
292
|
+
r["approval.json"]["events"] = L{}
|
|
293
|
+
}, "approvals and writers need revisions.plan"},
|
|
294
|
+
{"assessment without plan is fine", func(f *fixture, r map[string]J) {
|
|
295
|
+
r["run.json"]["revisions"] = J{}
|
|
296
|
+
r["run.json"]["assessment_only"] = true
|
|
297
|
+
r["approval.json"]["events"] = L{}
|
|
298
|
+
r["dispatch.json"]["attempts"] = L{}
|
|
299
|
+
}, ""},
|
|
300
|
+
{"terminal outcome missing", func(f *fixture, r map[string]J) {
|
|
301
|
+
r["run.json"]["execution_outcome"] = "READY_FOR_USER_REVIEW"
|
|
302
|
+
}, "approved tasks without exactly one recorded outcome: ['T-1']"},
|
|
303
|
+
{"terminal outcome recorded", func(f *fixture, r map[string]J) {
|
|
304
|
+
r["run.json"]["execution_outcome"] = "READY_FOR_USER_REVIEW"
|
|
305
|
+
r["run.json"]["task_outcomes"] = J{"T-1": "completed"}
|
|
306
|
+
}, ""},
|
|
307
|
+
{"unsettled attempt blocks leaving RUNNING", func(f *fixture, r map[string]J) {
|
|
308
|
+
r["run.json"]["execution_outcome"] = "STOPPED_BY_USER"
|
|
309
|
+
r["run.json"]["task_outcomes"] = J{"T-1": "completed"}
|
|
310
|
+
addAttempt(r, J{"task_id": "R-9", "attempt_id": "A1", "role": "reviewer", "status": "returned", "receipt": "receipts/R-1.json"})
|
|
311
|
+
}, "before the run leaves RUNNING: ['R-9#A1']"},
|
|
312
|
+
{"unknown and timed-out allowed after stop", func(f *fixture, r map[string]J) {
|
|
313
|
+
approval(r)["status"] = "stopped"
|
|
314
|
+
r["approval.json"]["events"] = append(r["approval.json"]["events"].(L), J{"id": "S-1", "kind": "stop"})
|
|
315
|
+
r["run.json"]["execution_outcome"] = "STOPPED_BY_USER"
|
|
316
|
+
r["run.json"]["task_outcomes"] = J{"T-1": "not-started"}
|
|
317
|
+
addAttempt(r, J{"task_id": "T-1", "attempt_id": "A2", "role": "implementer", "status": "unknown", "authorized_by": "AP-1"})
|
|
318
|
+
addAttempt(r, J{"task_id": "R-9", "attempt_id": "A1", "role": "reviewer", "status": "timed-out"})
|
|
319
|
+
}, ""},
|
|
320
|
+
{"scorecard needs revisions.rubric", func(f *fixture, r map[string]J) {
|
|
321
|
+
r["scorecard.json"] = J{"readiness_verdict": "NOT_ASSESSED"}
|
|
322
|
+
}, "scorecard needs revisions.rubric"},
|
|
323
|
+
{"scorecard rubric digest", func(f *fixture, r map[string]J) {
|
|
324
|
+
r["scorecard.json"] = J{"readiness_verdict": "NOT_ASSESSED"}
|
|
325
|
+
r["run.json"]["revisions"].(J)["rubric"] = J{"rev": 1, "file": "revisions/rubric-r1.json", "sha256": strings.Repeat("0", 64)}
|
|
326
|
+
}, "revisions.rubric digest mismatch"},
|
|
327
|
+
{"scorecard verdict differs", func(f *fixture, r map[string]J) {
|
|
328
|
+
r["scorecard.json"] = J{"readiness_verdict": "NOT_READY"}
|
|
329
|
+
r["run.json"]["revisions"].(J)["rubric"] = J{"rev": 1, "file": "revisions/rubric-r1.json",
|
|
330
|
+
"sha256": digest(f.t, filepath.Join(f.run, "revisions/rubric-r1.json"))}
|
|
331
|
+
}, "readiness_verdict differs"},
|
|
332
|
+
{"scorecard consistent", func(f *fixture, r map[string]J) {
|
|
333
|
+
r["scorecard.json"] = J{"readiness_verdict": "NOT_ASSESSED"}
|
|
334
|
+
r["run.json"]["revisions"].(J)["rubric"] = J{"rev": 1, "file": "revisions/rubric-r1.json",
|
|
335
|
+
"sha256": digest(f.t, filepath.Join(f.run, "revisions/rubric-r1.json"))}
|
|
336
|
+
}, ""},
|
|
337
|
+
{"schema tag and enum", func(f *fixture, r map[string]J) {
|
|
338
|
+
r["cycles.json"]["schema"] = "overhaul.cycle/1"
|
|
339
|
+
r["run.json"]["mode"] = "yolo"
|
|
340
|
+
}, "run.json: mode='yolo' not in ['branch', 'full', 'pr']"},
|
|
341
|
+
{"ineligible coverage without reason", func(f *fixture, r map[string]J) {
|
|
342
|
+
r["coverage.json"]["files"] = L{J{"path": "src/a.py", "eligible": false}}
|
|
343
|
+
}, "coverage src/a.py: ineligible without an exclusion reason"},
|
|
344
|
+
}
|
|
345
|
+
for _, c := range cases {
|
|
346
|
+
t.Run(c.name, func(t *testing.T) {
|
|
347
|
+
f := newFixture(t)
|
|
348
|
+
r := f.records()
|
|
349
|
+
c.mutate(f, r)
|
|
350
|
+
code, out := f.publish(r, nil)
|
|
351
|
+
if c.want == "" {
|
|
352
|
+
if code != 0 {
|
|
353
|
+
t.Fatalf("want publish ok, got %d:\n%s", code, out)
|
|
354
|
+
}
|
|
355
|
+
return
|
|
356
|
+
}
|
|
357
|
+
if code != 1 || !strings.Contains(out, "VIOLATION: ") || !strings.Contains(out, c.want) {
|
|
358
|
+
t.Fatalf("want exit 1 with %q, got %d:\n%s", c.want, code, out)
|
|
359
|
+
}
|
|
360
|
+
})
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
func TestCrossGenerationRules(t *testing.T) {
|
|
365
|
+
cases := []struct {
|
|
366
|
+
name string
|
|
367
|
+
mutate func(f *fixture, r map[string]J)
|
|
368
|
+
want string
|
|
369
|
+
}{
|
|
370
|
+
{"cycles append-only", func(f *fixture, r map[string]J) { r["cycles.json"]["cycles"] = L{J{"n": 2}} }, "cycles.json is append-only"},
|
|
371
|
+
{"cycles may grow", func(f *fixture, r map[string]J) {
|
|
372
|
+
r["cycles.json"]["cycles"] = L{J{"n": 1}, J{"n": 2}}
|
|
373
|
+
}, ""},
|
|
374
|
+
{"finding removed", func(f *fixture, r map[string]J) { r["findings.json"]["findings"] = L{} }, "finding F-1: removed or history rewritten"},
|
|
375
|
+
{"history rewritten", func(f *fixture, r map[string]J) {
|
|
376
|
+
finding(r)["history"] = L{J{"status": "candidate", "reason": "edited"}, J{"status": "confirmed"}}
|
|
377
|
+
}, "finding F-1: removed or history rewritten"},
|
|
378
|
+
{"severity change without history", func(f *fixture, r map[string]J) { finding(r)["severity"] = "medium" }, "severity or kind changed without a history entry"},
|
|
379
|
+
{"severity change with history", func(f *fixture, r map[string]J) {
|
|
380
|
+
finding(r)["severity"] = "medium"
|
|
381
|
+
finding(r)["history"] = L{J{"status": "candidate"}, J{"status": "confirmed"}, J{"status": "confirmed", "reason": "re-assessed"}}
|
|
382
|
+
}, ""},
|
|
383
|
+
{"approval events append-only", func(f *fixture, r map[string]J) { approval(r)["quote"] = "something else" }, "approval.json events are append-only"},
|
|
384
|
+
{"approval status may change", func(f *fixture, r map[string]J) { approval(r)["status"] = "revoked" }, ""},
|
|
385
|
+
{"immutable revision changed", func(f *fixture, r map[string]J) {
|
|
386
|
+
write(f.t, filepath.Join(f.run, "revisions/rubric-r1.json"), J{"schema": "overhaul.rubric/1", "rev": 1, "controls": L{}})
|
|
387
|
+
}, "immutable revision changed: revisions/rubric-r1.json"},
|
|
388
|
+
{"run_id pinned", func(f *fixture, r map[string]J) { r["run.json"]["run_id"] = "run-2" }, "run_id changed"},
|
|
389
|
+
{"mode pinned", func(f *fixture, r map[string]J) { r["run.json"]["mode"] = "pr" }, "mode changed"},
|
|
390
|
+
{"repo_root pinned", func(f *fixture, r map[string]J) {
|
|
391
|
+
r["run.json"]["repo_root"] = filepath.Join(f.repo, "src")
|
|
392
|
+
}, "repo_root changed"},
|
|
393
|
+
{"assessment_only pinned", func(f *fixture, r map[string]J) {
|
|
394
|
+
r["run.json"]["assessment_only"] = true
|
|
395
|
+
approval(r)["status"] = "revoked"
|
|
396
|
+
r["dispatch.json"]["attempts"] = L{}
|
|
397
|
+
}, "assessment_only changed"},
|
|
398
|
+
{"coverage denominator shrank", func(f *fixture, r map[string]J) { r["coverage.json"]["files"] = L{} }, "coverage denominator shrank: ['src/a.py']"},
|
|
399
|
+
{"coverage exclusion keeps denominator", func(f *fixture, r map[string]J) {
|
|
400
|
+
r["coverage.json"]["files"] = L{J{"path": "src/a.py", "eligible": false, "exclusion": J{"reason": "generated"}}}
|
|
401
|
+
}, ""},
|
|
402
|
+
}
|
|
403
|
+
for _, c := range cases {
|
|
404
|
+
t.Run(c.name, func(t *testing.T) {
|
|
405
|
+
f := newFixture(t)
|
|
406
|
+
f.mustPublish(f.records())
|
|
407
|
+
r := f.records()
|
|
408
|
+
c.mutate(f, r)
|
|
409
|
+
code, out := f.publish(r, nil)
|
|
410
|
+
if c.want == "" {
|
|
411
|
+
if code != 0 {
|
|
412
|
+
t.Fatalf("want publish ok, got %d:\n%s", code, out)
|
|
413
|
+
}
|
|
414
|
+
return
|
|
415
|
+
}
|
|
416
|
+
if code != 1 || !strings.Contains(out, c.want) {
|
|
417
|
+
t.Fatalf("want exit 1 with %q, got %d:\n%s", c.want, code, out)
|
|
418
|
+
}
|
|
419
|
+
})
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
func TestStaleStage(t *testing.T) {
|
|
424
|
+
f := newFixture(t)
|
|
425
|
+
f.mustPublish(f.records())
|
|
426
|
+
if code, _, errs := f.cmd("stage", f.run); code != 0 {
|
|
427
|
+
t.Fatal(errs)
|
|
428
|
+
}
|
|
429
|
+
write(t, filepath.Join(f.run, "CURRENT"), "g0000\n")
|
|
430
|
+
code, _, errs := f.cmd("publish", f.run)
|
|
431
|
+
if code != 1 || !strings.Contains(errs, "stale stage: CURRENT moved from g0001 to g0000") {
|
|
432
|
+
t.Fatalf("got %d %s", code, errs)
|
|
433
|
+
}
|
|
434
|
+
if code, _, errs := f.cmd("stage", f.run); code != 2 || !strings.Contains(errs, "g0001 exists but CURRENT is g0000: interrupted publish") {
|
|
435
|
+
t.Fatalf("restage: %d %s", code, errs)
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
func TestStageCopiesWithoutViewsOrManifest(t *testing.T) {
|
|
440
|
+
f := newFixture(t)
|
|
441
|
+
stamp := "overhaul-stamp run=run-1 generation=g0001 plan=r1:" + digest(t, filepath.Join(f.run, "revisions/plan-r1.json"))
|
|
442
|
+
if code, out := f.publish(f.records(), map[string]string{"views/review.md": "# Review\n" + stamp + "\n"}); code != 0 {
|
|
443
|
+
t.Fatal(out)
|
|
444
|
+
}
|
|
445
|
+
code, out, errs := f.cmd("stage", f.run)
|
|
446
|
+
if code != 0 {
|
|
447
|
+
t.Fatal(errs)
|
|
448
|
+
}
|
|
449
|
+
tmp := strings.TrimSpace(out)
|
|
450
|
+
if filepath.Clean(tmp) != filepath.Join(f.run, "g0002.tmp") {
|
|
451
|
+
t.Fatalf("stage path %q", tmp)
|
|
452
|
+
}
|
|
453
|
+
base, _ := os.ReadFile(filepath.Join(tmp, ".base"))
|
|
454
|
+
if string(base) != "g0001" || isDir(filepath.Join(tmp, "views")) || isFile(filepath.Join(tmp, "manifest.json")) || !isFile(filepath.Join(tmp, "run.json")) {
|
|
455
|
+
t.Fatalf("bad stage contents (base %q)", base)
|
|
456
|
+
}
|
|
457
|
+
if code, _, errs := f.cmd("stage", f.run); code != 2 || !strings.Contains(errs, "staged generation already exists") {
|
|
458
|
+
t.Fatalf("double stage: %d %s", code, errs)
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
func TestTamperedGenerationDetected(t *testing.T) {
|
|
463
|
+
f := newFixture(t)
|
|
464
|
+
f.mustPublish(f.records())
|
|
465
|
+
p := filepath.Join(f.run, "g0001/cycles.json")
|
|
466
|
+
b, _ := os.ReadFile(p)
|
|
467
|
+
write(t, p, string(b)+" ")
|
|
468
|
+
code, out, _ := f.cmd("validate", f.run)
|
|
469
|
+
if code != 1 || !strings.Contains(out, "VIOLATION: mixed or modified generation: cycles.json does not match manifest") {
|
|
470
|
+
t.Fatalf("got %d %s", code, out)
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
func TestViewStamp(t *testing.T) {
|
|
475
|
+
f := newFixture(t)
|
|
476
|
+
code, out := f.publish(f.records(), map[string]string{"views/review.md": "overhaul-stamp run=run-1 generation=g0000 plan=none\n"})
|
|
477
|
+
if code != 1 || !strings.Contains(out, "view review.md: stamp does not match this generation") {
|
|
478
|
+
t.Fatalf("got %d %s", code, out)
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// Views from the renderer must carry the stamp this validator expects.
|
|
483
|
+
func TestRenderedViewsPassValidation(t *testing.T) {
|
|
484
|
+
f := newFixture(t)
|
|
485
|
+
code, out, errs := f.cmd("stage", f.run)
|
|
486
|
+
if code != 0 {
|
|
487
|
+
t.Fatalf("stage: %d %s", code, errs)
|
|
488
|
+
}
|
|
489
|
+
tmp := strings.TrimSpace(out)
|
|
490
|
+
for name, doc := range f.records() {
|
|
491
|
+
write(t, filepath.Join(tmp, name), doc)
|
|
492
|
+
}
|
|
493
|
+
for _, kind := range []string{"review", "report"} {
|
|
494
|
+
var o, e bytes.Buffer
|
|
495
|
+
if c := render.Run([]string{f.run, tmp, kind}, &o, &e); c != 0 {
|
|
496
|
+
t.Fatalf("render %s: %d %s", kind, c, e.String())
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
if code, out, errs := f.cmd("publish", f.run); code != 0 {
|
|
500
|
+
t.Fatalf("publish: %d %s%s", code, out, errs)
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
func TestInterruptedPublish(t *testing.T) {
|
|
505
|
+
f := newFixture(t)
|
|
506
|
+
f.mustPublish(f.records())
|
|
507
|
+
if err := os.Mkdir(filepath.Join(f.run, "g0002"), 0o755); err != nil {
|
|
508
|
+
t.Fatal(err)
|
|
509
|
+
}
|
|
510
|
+
code, _, errs := f.cmd("stage", f.run)
|
|
511
|
+
if code != 2 || !strings.Contains(errs, "interrupted publish") {
|
|
512
|
+
t.Fatalf("stage: %d %s", code, errs)
|
|
513
|
+
}
|
|
514
|
+
code, out, _ := f.cmd("validate", f.run)
|
|
515
|
+
if code != 0 || !strings.Contains(out, "NOTE: g0002 is newer than CURRENT") || !strings.Contains(out, "valid g0001") {
|
|
516
|
+
t.Fatalf("validate: %d %s", code, out)
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
func TestPublishNeedsExactlyOneStage(t *testing.T) {
|
|
521
|
+
f := newFixture(t)
|
|
522
|
+
if err := os.MkdirAll(filepath.Join(f.run, "revisions"), 0o755); err != nil {
|
|
523
|
+
t.Fatal(err)
|
|
524
|
+
}
|
|
525
|
+
code, _, errs := f.cmd("publish", f.run)
|
|
526
|
+
if code != 2 || !strings.Contains(errs, "expected exactly one staged generation, found []") {
|
|
527
|
+
t.Fatalf("got %d %s", code, errs)
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
func TestBadPath(t *testing.T) {
|
|
532
|
+
repo := t.TempDir()
|
|
533
|
+
outside := t.TempDir()
|
|
534
|
+
write(t, filepath.Join(repo, "src/a.py"), "x\n")
|
|
535
|
+
write(t, filepath.Join(outside, "secret.py"), "x\n")
|
|
536
|
+
if err := os.Symlink(filepath.Join(outside, "secret.py"), filepath.Join(repo, "src/out.py")); err != nil {
|
|
537
|
+
t.Fatal(err)
|
|
538
|
+
}
|
|
539
|
+
cases := []struct {
|
|
540
|
+
path any
|
|
541
|
+
want string
|
|
542
|
+
}{
|
|
543
|
+
{"src/a.py", ""},
|
|
544
|
+
{"src/new.py", ""},
|
|
545
|
+
{"app/blog/[slug]/page.tsx", ""},
|
|
546
|
+
{"src/*.py", "not an exact relative file path"},
|
|
547
|
+
{"src/a?.py", "not an exact relative file path"},
|
|
548
|
+
{"/etc/passwd", "not an exact relative file path"},
|
|
549
|
+
{"src/", "not an exact relative file path"},
|
|
550
|
+
{"", "not an exact relative file path"},
|
|
551
|
+
{json.Number("3"), "not an exact relative file path"},
|
|
552
|
+
{"../x.py", "not normalized or escapes the repository"},
|
|
553
|
+
{"src/../src/a.py", "not normalized or escapes the repository"},
|
|
554
|
+
{"./src/a.py", "not normalized or escapes the repository"},
|
|
555
|
+
{"src", "names a directory or git metadata"},
|
|
556
|
+
{".git/config", "names a directory or git metadata"},
|
|
557
|
+
{"src/.git/HEAD", "names a directory or git metadata"},
|
|
558
|
+
{"src/out.py", "resolves outside the repository (symlink or traversal)"},
|
|
559
|
+
}
|
|
560
|
+
for _, c := range cases {
|
|
561
|
+
if got := BadPath(c.path, repo); got != c.want {
|
|
562
|
+
t.Errorf("BadPath(%v) = %q, want %q", c.path, got, c.want)
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
func TestRepr(t *testing.T) {
|
|
568
|
+
cases := map[string]any{
|
|
569
|
+
"None": nil, "True": true, "'a'": "a", `"it's"`: "it's", "['a', 'b']": L{"a", "b"},
|
|
570
|
+
"3": json.Number("3"), `'a\\b'`: `a\b`,
|
|
571
|
+
}
|
|
572
|
+
for want, v := range cases {
|
|
573
|
+
if got := Repr(v); got != want {
|
|
574
|
+
t.Errorf("Repr(%v) = %s, want %s", v, got, want)
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
}
|