devrites 4.3.0 → 4.4.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 +2 -3
- package/README.md +1 -1
- package/docs/cli.md +15 -0
- package/docs/engine/commands.md +17 -3
- package/engine/commands.go +1 -1
- package/engine/internal/lib/open_visual.go +254 -0
- package/engine/internal/lib/open_visual_test.go +280 -0
- package/engine/internal/lib/visual_outline.go +163 -0
- package/engine/internal/lib/visual_outline_test.go +161 -0
- package/engine/main.go +4 -0
- package/engine/root_routing_test.go +49 -14
- package/engine/testdata/visual/open-visual-smoke.html +675 -0
- package/engine/testdata/visual/open-visual-smoke.outline.md +54 -0
- package/pack/.claude/skills/devrites-lib/SKILL.md +2 -0
- package/pack/.claude/skills/devrites-lib/reference/visual-playbooks/code.md +46 -0
- package/pack/.claude/skills/devrites-lib/reference/visual-playbooks/comparison.md +44 -0
- package/pack/.claude/skills/devrites-lib/reference/visual-playbooks/diagram.md +53 -0
- package/pack/.claude/skills/devrites-lib/reference/visual-playbooks/index.md +57 -0
- package/pack/.claude/skills/devrites-lib/reference/visual-playbooks/input.md +47 -0
- package/pack/.claude/skills/devrites-lib/reference/visual-playbooks/outline-template.md +106 -0
- package/pack/.claude/skills/devrites-lib/reference/visual-playbooks/plan.md +49 -0
- package/pack/.claude/skills/devrites-lib/reference/visual-playbooks/slides.md +42 -0
- package/pack/.claude/skills/devrites-lib/reference/visual-playbooks/table.md +44 -0
- package/pack/.claude/skills/devrites-lib/reference/workspace-artifact-schema.md +24 -9
- package/pack/.claude/skills/rite-define/SKILL.md +10 -1
- package/pack/.claude/skills/rite-explain/SKILL.md +23 -7
- package/pack/.claude/skills/rite-explain/reference/intake.md +11 -2
- package/pack/.claude/skills/rite-spec/reference/spec-template.md +5 -2
- package/pack/.claude/skills/rite-spec/reference/state-workspace.md +26 -3
- package/pack/generated/claude/skills/devrites-lib/SKILL.md +2 -0
- package/pack/generated/claude/skills/devrites-lib/reference/visual-playbooks/code.md +46 -0
- package/pack/generated/claude/skills/devrites-lib/reference/visual-playbooks/comparison.md +44 -0
- package/pack/generated/claude/skills/devrites-lib/reference/visual-playbooks/diagram.md +53 -0
- package/pack/generated/claude/skills/devrites-lib/reference/visual-playbooks/index.md +57 -0
- package/pack/generated/claude/skills/devrites-lib/reference/visual-playbooks/input.md +47 -0
- package/pack/generated/claude/skills/devrites-lib/reference/visual-playbooks/outline-template.md +106 -0
- package/pack/generated/claude/skills/devrites-lib/reference/visual-playbooks/plan.md +49 -0
- package/pack/generated/claude/skills/devrites-lib/reference/visual-playbooks/slides.md +42 -0
- package/pack/generated/claude/skills/devrites-lib/reference/visual-playbooks/table.md +44 -0
- package/pack/generated/claude/skills/devrites-lib/reference/workspace-artifact-schema.md +24 -9
- package/pack/generated/claude/skills/rite-define/SKILL.md +10 -1
- package/pack/generated/claude/skills/rite-explain/SKILL.md +23 -7
- package/pack/generated/claude/skills/rite-explain/reference/intake.md +11 -2
- package/pack/generated/claude/skills/rite-spec/reference/spec-template.md +5 -2
- package/pack/generated/claude/skills/rite-spec/reference/state-workspace.md +26 -3
- package/pack/generated/codex/skills/devrites-lib/SKILL.md +2 -0
- package/pack/generated/codex/skills/devrites-lib/reference/visual-playbooks/code.md +46 -0
- package/pack/generated/codex/skills/devrites-lib/reference/visual-playbooks/comparison.md +44 -0
- package/pack/generated/codex/skills/devrites-lib/reference/visual-playbooks/diagram.md +53 -0
- package/pack/generated/codex/skills/devrites-lib/reference/visual-playbooks/index.md +57 -0
- package/pack/generated/codex/skills/devrites-lib/reference/visual-playbooks/input.md +47 -0
- package/pack/generated/codex/skills/devrites-lib/reference/visual-playbooks/outline-template.md +106 -0
- package/pack/generated/codex/skills/devrites-lib/reference/visual-playbooks/plan.md +49 -0
- package/pack/generated/codex/skills/devrites-lib/reference/visual-playbooks/slides.md +42 -0
- package/pack/generated/codex/skills/devrites-lib/reference/visual-playbooks/table.md +44 -0
- package/pack/generated/codex/skills/devrites-lib/reference/workspace-artifact-schema.md +24 -9
- package/pack/generated/codex/skills/rite-define/SKILL.md +10 -1
- package/pack/generated/codex/skills/rite-explain/SKILL.md +23 -7
- package/pack/generated/codex/skills/rite-explain/reference/intake.md +11 -2
- package/pack/generated/codex/skills/rite-spec/reference/spec-template.md +5 -2
- package/pack/generated/codex/skills/rite-spec/reference/state-workspace.md +26 -3
- package/package.json +1 -1
- package/scripts/install-lib.sh +1 -1
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
package lib
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"regexp"
|
|
5
|
+
"strings"
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
// htmlIDAttrRE matches id="..." or id='...' attributes without a full HTML parser.
|
|
9
|
+
var htmlIDAttrRE = regexp.MustCompile(`(?i)\bid\s*=\s*(?:"([^"]*)"|'([^']*)')`)
|
|
10
|
+
|
|
11
|
+
// VisualIDConsistency summarizes outline ## ID inventory vs HTML id attributes.
|
|
12
|
+
//
|
|
13
|
+
// Policy: warn only for inventory → HTML mismatches. HTML-only ids (for example
|
|
14
|
+
// SVG marker defs such as id="arrow") are treated as decorative and are not
|
|
15
|
+
// reported, to keep open-visual tips quiet on intentional landmark-only
|
|
16
|
+
// inventories.
|
|
17
|
+
type VisualIDConsistency struct {
|
|
18
|
+
Inventory []string
|
|
19
|
+
HTML []string
|
|
20
|
+
MissingInHTML []string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// ParseOutlineInventoryIDs extracts HTML ids from the outline ## ID inventory table.
|
|
24
|
+
func ParseOutlineInventoryIDs(outline string) []string {
|
|
25
|
+
section := extractMarkdownSection(outline, "ID inventory")
|
|
26
|
+
if section == "" {
|
|
27
|
+
return nil
|
|
28
|
+
}
|
|
29
|
+
return parseMarkdownFirstColumnIDs(section)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ScanHTMLIDs returns unique id="..." / id='...' attribute values in document order.
|
|
33
|
+
func ScanHTMLIDs(html string) []string {
|
|
34
|
+
matches := htmlIDAttrRE.FindAllStringSubmatch(html, -1)
|
|
35
|
+
if len(matches) == 0 {
|
|
36
|
+
return nil
|
|
37
|
+
}
|
|
38
|
+
seen := make(map[string]struct{}, len(matches))
|
|
39
|
+
ids := make([]string, 0, len(matches))
|
|
40
|
+
for _, m := range matches {
|
|
41
|
+
id := m[1]
|
|
42
|
+
if id == "" {
|
|
43
|
+
id = m[2]
|
|
44
|
+
}
|
|
45
|
+
id = strings.TrimSpace(id)
|
|
46
|
+
if id == "" {
|
|
47
|
+
continue
|
|
48
|
+
}
|
|
49
|
+
if _, ok := seen[id]; ok {
|
|
50
|
+
continue
|
|
51
|
+
}
|
|
52
|
+
seen[id] = struct{}{}
|
|
53
|
+
ids = append(ids, id)
|
|
54
|
+
}
|
|
55
|
+
return ids
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// CheckVisualIDConsistency compares outline inventory ids against HTML attributes.
|
|
59
|
+
func CheckVisualIDConsistency(html, outline string) VisualIDConsistency {
|
|
60
|
+
inventory := ParseOutlineInventoryIDs(outline)
|
|
61
|
+
htmlIDs := ScanHTMLIDs(html)
|
|
62
|
+
report := VisualIDConsistency{
|
|
63
|
+
Inventory: inventory,
|
|
64
|
+
HTML: htmlIDs,
|
|
65
|
+
}
|
|
66
|
+
if len(inventory) == 0 {
|
|
67
|
+
return report
|
|
68
|
+
}
|
|
69
|
+
htmlSet := make(map[string]struct{}, len(htmlIDs))
|
|
70
|
+
for _, id := range htmlIDs {
|
|
71
|
+
htmlSet[id] = struct{}{}
|
|
72
|
+
}
|
|
73
|
+
for _, id := range inventory {
|
|
74
|
+
if _, ok := htmlSet[id]; !ok {
|
|
75
|
+
report.MissingInHTML = append(report.MissingInHTML, id)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return report
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
func extractMarkdownSection(md, heading string) string {
|
|
82
|
+
lines := strings.Split(md, "\n")
|
|
83
|
+
start := -1
|
|
84
|
+
for i, line := range lines {
|
|
85
|
+
trimmed := strings.TrimSpace(line)
|
|
86
|
+
if !strings.HasPrefix(trimmed, "## ") {
|
|
87
|
+
continue
|
|
88
|
+
}
|
|
89
|
+
h := strings.TrimSpace(strings.TrimPrefix(trimmed, "## "))
|
|
90
|
+
if strings.EqualFold(h, heading) {
|
|
91
|
+
start = i + 1
|
|
92
|
+
continue
|
|
93
|
+
}
|
|
94
|
+
if start >= 0 {
|
|
95
|
+
return strings.Join(lines[start:i], "\n")
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if start < 0 {
|
|
99
|
+
return ""
|
|
100
|
+
}
|
|
101
|
+
return strings.Join(lines[start:], "\n")
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
func parseMarkdownFirstColumnIDs(section string) []string {
|
|
105
|
+
var ids []string
|
|
106
|
+
seen := make(map[string]struct{})
|
|
107
|
+
for _, line := range strings.Split(section, "\n") {
|
|
108
|
+
line = strings.TrimSpace(line)
|
|
109
|
+
if !strings.HasPrefix(line, "|") {
|
|
110
|
+
continue
|
|
111
|
+
}
|
|
112
|
+
cells := splitMarkdownRow(line)
|
|
113
|
+
if len(cells) == 0 {
|
|
114
|
+
continue
|
|
115
|
+
}
|
|
116
|
+
raw := strings.TrimSpace(cells[0])
|
|
117
|
+
if raw == "" || isMarkdownSeparatorCell(raw) {
|
|
118
|
+
continue
|
|
119
|
+
}
|
|
120
|
+
normalizedHeader := strings.ToLower(strings.ReplaceAll(raw, "`", ""))
|
|
121
|
+
normalizedHeader = strings.Join(strings.Fields(normalizedHeader), " ")
|
|
122
|
+
if normalizedHeader == "html id" || normalizedHeader == "id" {
|
|
123
|
+
continue
|
|
124
|
+
}
|
|
125
|
+
id := strings.TrimSpace(strings.Trim(raw, "`"))
|
|
126
|
+
if id == "" {
|
|
127
|
+
continue
|
|
128
|
+
}
|
|
129
|
+
if _, ok := seen[id]; ok {
|
|
130
|
+
continue
|
|
131
|
+
}
|
|
132
|
+
seen[id] = struct{}{}
|
|
133
|
+
ids = append(ids, id)
|
|
134
|
+
}
|
|
135
|
+
return ids
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
func splitMarkdownRow(line string) []string {
|
|
139
|
+
line = strings.TrimSpace(line)
|
|
140
|
+
line = strings.TrimPrefix(line, "|")
|
|
141
|
+
line = strings.TrimSuffix(line, "|")
|
|
142
|
+
parts := strings.Split(line, "|")
|
|
143
|
+
cells := make([]string, 0, len(parts))
|
|
144
|
+
for _, part := range parts {
|
|
145
|
+
cells = append(cells, strings.TrimSpace(part))
|
|
146
|
+
}
|
|
147
|
+
return cells
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
func isMarkdownSeparatorCell(cell string) bool {
|
|
151
|
+
if cell == "" {
|
|
152
|
+
return false
|
|
153
|
+
}
|
|
154
|
+
for _, r := range cell {
|
|
155
|
+
switch r {
|
|
156
|
+
case '-', ':', ' ':
|
|
157
|
+
continue
|
|
158
|
+
default:
|
|
159
|
+
return false
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return strings.ContainsRune(cell, '-')
|
|
163
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
package lib
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"bytes"
|
|
5
|
+
"path/filepath"
|
|
6
|
+
"strings"
|
|
7
|
+
"testing"
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
func TestParseOutlineInventoryIDs(t *testing.T) {
|
|
11
|
+
outline := `# Title
|
|
12
|
+
|
|
13
|
+
## Purpose
|
|
14
|
+
demo
|
|
15
|
+
|
|
16
|
+
## ID inventory
|
|
17
|
+
|
|
18
|
+
| HTML ` + "`id`" + ` | Meaning |
|
|
19
|
+
| --- | --- |
|
|
20
|
+
| ` + "`viz-title`" + ` | Header |
|
|
21
|
+
| ` + "`diagram-overview`" + ` | Overview |
|
|
22
|
+
| viz-title | duplicate ignored |
|
|
23
|
+
|
|
24
|
+
## Relationships
|
|
25
|
+
| From | To | Note |
|
|
26
|
+
| --- | --- | --- |
|
|
27
|
+
| a | b | c |
|
|
28
|
+
`
|
|
29
|
+
got := ParseOutlineInventoryIDs(outline)
|
|
30
|
+
want := []string{"viz-title", "diagram-overview"}
|
|
31
|
+
if len(got) != len(want) {
|
|
32
|
+
t.Fatalf("ids = %#v, want %#v", got, want)
|
|
33
|
+
}
|
|
34
|
+
for i := range want {
|
|
35
|
+
if got[i] != want[i] {
|
|
36
|
+
t.Fatalf("ids = %#v, want %#v", got, want)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
func TestScanHTMLIDs(t *testing.T) {
|
|
42
|
+
html := `<!doctype html><div id="viz-title"></div><svg><marker id='arrow'></marker><g id="viz-title"></g></svg>`
|
|
43
|
+
got := ScanHTMLIDs(html)
|
|
44
|
+
want := []string{"viz-title", "arrow"}
|
|
45
|
+
if len(got) != len(want) {
|
|
46
|
+
t.Fatalf("ids = %#v, want %#v", got, want)
|
|
47
|
+
}
|
|
48
|
+
for i := range want {
|
|
49
|
+
if got[i] != want[i] {
|
|
50
|
+
t.Fatalf("ids = %#v, want %#v", got, want)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
func TestCheckVisualIDConsistencyMatch(t *testing.T) {
|
|
56
|
+
html := `<section id="viz-title"></section><section id="diagram-overview"></section><marker id="arrow"></marker>`
|
|
57
|
+
outline := `## ID inventory
|
|
58
|
+
| HTML id | Meaning |
|
|
59
|
+
| --- | --- |
|
|
60
|
+
| viz-title | Header |
|
|
61
|
+
| diagram-overview | Overview |
|
|
62
|
+
`
|
|
63
|
+
report := CheckVisualIDConsistency(html, outline)
|
|
64
|
+
if len(report.MissingInHTML) != 0 {
|
|
65
|
+
t.Fatalf("MissingInHTML = %#v, want empty", report.MissingInHTML)
|
|
66
|
+
}
|
|
67
|
+
if len(report.Inventory) != 2 {
|
|
68
|
+
t.Fatalf("Inventory = %#v, want 2 ids", report.Inventory)
|
|
69
|
+
}
|
|
70
|
+
// HTML-only decorative ids are not reported.
|
|
71
|
+
if len(report.HTML) != 3 {
|
|
72
|
+
t.Fatalf("HTML = %#v, want viz-title, diagram-overview, arrow", report.HTML)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
func TestCheckVisualIDConsistencyMissingInHTML(t *testing.T) {
|
|
77
|
+
html := `<section id="viz-title"></section>`
|
|
78
|
+
outline := `## ID inventory
|
|
79
|
+
| HTML id | Meaning |
|
|
80
|
+
| --- | --- |
|
|
81
|
+
| viz-title | Header |
|
|
82
|
+
| viz-open-questions | Open questions |
|
|
83
|
+
`
|
|
84
|
+
report := CheckVisualIDConsistency(html, outline)
|
|
85
|
+
if len(report.MissingInHTML) != 1 || report.MissingInHTML[0] != "viz-open-questions" {
|
|
86
|
+
t.Fatalf("MissingInHTML = %#v, want [viz-open-questions]", report.MissingInHTML)
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
func TestCheckVisualIDConsistencyMissingOutlineSection(t *testing.T) {
|
|
91
|
+
report := CheckVisualIDConsistency(`<div id="x"></div>`, "# Title\n\ndemo\n")
|
|
92
|
+
if len(report.Inventory) != 0 || len(report.MissingInHTML) != 0 {
|
|
93
|
+
t.Fatalf("report = %#v, want empty inventory/missing", report)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
func TestOpenVisualWarnsIDMismatch(t *testing.T) {
|
|
98
|
+
root, html := writeOpenVisualFixture(t, true)
|
|
99
|
+
t.Setenv("DEVRITES_WORKSPACE", filepath.Join(root, "work", "feature"))
|
|
100
|
+
|
|
101
|
+
outline := strings.TrimSuffix(html, ".html") + ".outline.md"
|
|
102
|
+
writeBasenameFile(t, filepath.Dir(outline), filepath.Base(outline), `# demo
|
|
103
|
+
|
|
104
|
+
## ID inventory
|
|
105
|
+
| HTML id | Meaning |
|
|
106
|
+
| --- | --- |
|
|
107
|
+
| viz-title | Header |
|
|
108
|
+
| missing-node | Absent from HTML |
|
|
109
|
+
`)
|
|
110
|
+
writeBasenameFile(t, filepath.Dir(html), filepath.Base(html), `<!doctype html><title>demo</title><section id="viz-title"></section>
|
|
111
|
+
`)
|
|
112
|
+
|
|
113
|
+
restore := swapOpenVisualOpener(func(string) error { return nil })
|
|
114
|
+
defer restore()
|
|
115
|
+
|
|
116
|
+
var stdout, stderr bytes.Buffer
|
|
117
|
+
code := OpenVisual(root, []string{"demo", "--no-open"}, &stdout, &stderr)
|
|
118
|
+
if code != 0 {
|
|
119
|
+
t.Fatalf("OpenVisual() = %d stderr=%q", code, stderr.String())
|
|
120
|
+
}
|
|
121
|
+
if !strings.Contains(stderr.String(), "outline inventory id(s) missing from HTML") {
|
|
122
|
+
t.Fatalf("stderr = %q, want missing-id warning", stderr.String())
|
|
123
|
+
}
|
|
124
|
+
if !strings.Contains(stderr.String(), "missing-node") {
|
|
125
|
+
t.Fatalf("stderr = %q, want missing-node listed", stderr.String())
|
|
126
|
+
}
|
|
127
|
+
if !strings.Contains(stdout.String(), "open-visual: ids=mismatch") {
|
|
128
|
+
t.Fatalf("stdout = %q, want ids=mismatch tip", stdout.String())
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
func TestOpenVisualIDsOkTip(t *testing.T) {
|
|
133
|
+
root, html := writeOpenVisualFixture(t, true)
|
|
134
|
+
t.Setenv("DEVRITES_WORKSPACE", filepath.Join(root, "work", "feature"))
|
|
135
|
+
|
|
136
|
+
outline := strings.TrimSuffix(html, ".html") + ".outline.md"
|
|
137
|
+
writeBasenameFile(t, filepath.Dir(outline), filepath.Base(outline), `# demo
|
|
138
|
+
|
|
139
|
+
## ID inventory
|
|
140
|
+
| HTML id | Meaning |
|
|
141
|
+
| --- | --- |
|
|
142
|
+
| viz-title | Header |
|
|
143
|
+
`)
|
|
144
|
+
writeBasenameFile(t, filepath.Dir(html), filepath.Base(html), `<!doctype html><title>demo</title><section id="viz-title"></section><marker id="arrow"></marker>
|
|
145
|
+
`)
|
|
146
|
+
|
|
147
|
+
restore := swapOpenVisualOpener(func(string) error { return nil })
|
|
148
|
+
defer restore()
|
|
149
|
+
|
|
150
|
+
var stdout, stderr bytes.Buffer
|
|
151
|
+
code := OpenVisual(root, []string{"demo", "--no-open"}, &stdout, &stderr)
|
|
152
|
+
if code != 0 {
|
|
153
|
+
t.Fatalf("OpenVisual() = %d stderr=%q", code, stderr.String())
|
|
154
|
+
}
|
|
155
|
+
if stderr.Len() != 0 {
|
|
156
|
+
t.Fatalf("stderr = %q, want empty when consistent", stderr.String())
|
|
157
|
+
}
|
|
158
|
+
if !strings.Contains(stdout.String(), "open-visual: ids=ok (1 inventory)") {
|
|
159
|
+
t.Fatalf("stdout = %q, want ids=ok tip", stdout.String())
|
|
160
|
+
}
|
|
161
|
+
}
|
package/engine/main.go
CHANGED
|
@@ -34,6 +34,8 @@ Usage:
|
|
|
34
34
|
devrites-engine state resolve <qid> "<ans>" Resolve an open question and update state atomically
|
|
35
35
|
devrites-engine state close <slug> Archive a shipped feature and clear ACTIVE
|
|
36
36
|
devrites-engine secret-scan [--staged] [--stdin] [slug] Scan exact staged blobs, stdin, or touched files; HIGH blocks
|
|
37
|
+
devrites-engine open-visual <path-or-name> [--slug <slug>] [--no-open]
|
|
38
|
+
Resolve a visual HTML file, optionally open it locally, print agent paths
|
|
37
39
|
devrites-engine version Print the engine binary's version
|
|
38
40
|
Exit codes:
|
|
39
41
|
0 ok / gate passed
|
|
@@ -87,6 +89,8 @@ func run(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
|
|
|
87
89
|
return cmdState(root, args[1:], stdout, stderr)
|
|
88
90
|
case "secret-scan":
|
|
89
91
|
return lib.SecretScan(root, args[1:], stdin, stdout, stderr)
|
|
92
|
+
case "open-visual":
|
|
93
|
+
return lib.OpenVisual(root, args[1:], stdout, stderr)
|
|
90
94
|
case "version", "--version":
|
|
91
95
|
fmt.Fprintln(stdout, version.Version)
|
|
92
96
|
return exitOK
|
|
@@ -21,6 +21,7 @@ func TestRootModeForCoversReadAndWriteSurfaces(t *testing.T) {
|
|
|
21
21
|
{name: "close", command: "state", args: []string{"close"}, want: rootStrict},
|
|
22
22
|
{name: "unknown state command", command: "state", args: []string{"unknown"}, want: rootUnused},
|
|
23
23
|
{name: "secret scan", command: "secret-scan", want: rootLenient},
|
|
24
|
+
{name: "open visual", command: "open-visual", want: rootLenient},
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
for _, test := range tests {
|
|
@@ -102,9 +103,9 @@ func TestNestedCommandFamiliesAreRoutedAndAdvertised(t *testing.T) {
|
|
|
102
103
|
for _, args := range [][]string{{"check", "candidate"}, {"check", "readiness"}, {"check", "seal"}, {"state", "resolve"}, {"state", "close"}} {
|
|
103
104
|
t.Run(strings.Join(args, "-"), func(t *testing.T) {
|
|
104
105
|
var stdout, stderr bytes.Buffer
|
|
105
|
-
|
|
106
|
+
code := run(args, strings.NewReader(""), &stdout, &stderr)
|
|
106
107
|
if strings.Contains(stderr.String(), "unknown command") || strings.Contains(stderr.String(), "unknown check") || strings.Contains(stderr.String(), "unknown state") {
|
|
107
|
-
t.Fatalf("%q was not routed: %s", args, stderr.String())
|
|
108
|
+
t.Fatalf("%q was not routed (exit=%d): %s", args, code, stderr.String())
|
|
108
109
|
}
|
|
109
110
|
})
|
|
110
111
|
}
|
|
@@ -153,13 +154,9 @@ func TestCheckCandidateRoutesAndPrintsIdentity(t *testing.T) {
|
|
|
153
154
|
if err := os.MkdirAll(workspace, 0o755); err != nil {
|
|
154
155
|
t.Fatal(err)
|
|
155
156
|
}
|
|
156
|
-
|
|
157
|
-
t.Fatal(err)
|
|
158
|
-
}
|
|
157
|
+
writeBasenameFile(t, project, "source.go", "package source\n")
|
|
159
158
|
manifest := "# Touched files\n\n## Touched files\nCandidate paths are declared below.\n\n## Candidate manifest\n| State | File | Slice | Reason |\n| --- | --- | --- | --- |\n| present | `source.go` | S-1 | Implementation. |\n"
|
|
160
|
-
|
|
161
|
-
t.Fatal(err)
|
|
162
|
-
}
|
|
159
|
+
writeBasenameFile(t, workspace, "touched-files.md", manifest)
|
|
163
160
|
t.Setenv("DEVRITES_ROOT", root)
|
|
164
161
|
var stdout, stderr bytes.Buffer
|
|
165
162
|
if code := run([]string{"check", "candidate", "feature"}, strings.NewReader(""), &stdout, &stderr); code != exitOK {
|
|
@@ -186,9 +183,7 @@ func TestCheckReadinessEmitBindingRoutesOnlyExactShape(t *testing.T) {
|
|
|
186
183
|
if err := os.MkdirAll(workspace, 0o755); err != nil {
|
|
187
184
|
t.Fatal(err)
|
|
188
185
|
}
|
|
189
|
-
|
|
190
|
-
t.Fatal(err)
|
|
191
|
-
}
|
|
186
|
+
writeBasenameFile(t, workspace, name, body)
|
|
192
187
|
}
|
|
193
188
|
t.Setenv("DEVRITES_ROOT", root)
|
|
194
189
|
|
|
@@ -217,9 +212,7 @@ func TestCheckReadinessEmitBindingRoutesOnlyExactShape(t *testing.T) {
|
|
|
217
212
|
}
|
|
218
213
|
}
|
|
219
214
|
|
|
220
|
-
|
|
221
|
-
t.Fatal(err)
|
|
222
|
-
}
|
|
215
|
+
removeBasename(t, workspace, "plan.md")
|
|
223
216
|
stdout.Reset()
|
|
224
217
|
stderr.Reset()
|
|
225
218
|
if code := run([]string{"check", "readiness", "--emit-binding", "feature"}, strings.NewReader(""), &stdout, &stderr); code != exitBlocked || stdout.Len() != 0 || !strings.Contains(stderr.String(), "readiness-binding: BLOCKED") {
|
|
@@ -245,3 +238,45 @@ func TestSecretScanRoutesPRBodyThroughStdin(t *testing.T) {
|
|
|
245
238
|
t.Fatal("secret material disclosed")
|
|
246
239
|
}
|
|
247
240
|
}
|
|
241
|
+
|
|
242
|
+
// writeBasenameFile writes contents under dir using only filepath.Base(name),
|
|
243
|
+
// via CreateTemp + WriteString + Close + Rename (no WriteFile/Create sinks).
|
|
244
|
+
func writeBasenameFile(t *testing.T, dir, name, contents string) string {
|
|
245
|
+
t.Helper()
|
|
246
|
+
base := filepath.Base(name)
|
|
247
|
+
dst := filepath.Join(dir, base)
|
|
248
|
+
tmp, err := os.CreateTemp(dir, "."+base+".tmp-*")
|
|
249
|
+
if err != nil {
|
|
250
|
+
t.Fatal(err)
|
|
251
|
+
}
|
|
252
|
+
written, werr := tmp.WriteString(contents)
|
|
253
|
+
if werr != nil {
|
|
254
|
+
if cerr := tmp.Close(); cerr != nil {
|
|
255
|
+
t.Fatalf("write: %v; close: %v", werr, cerr)
|
|
256
|
+
}
|
|
257
|
+
t.Fatal(werr)
|
|
258
|
+
}
|
|
259
|
+
if written != len(contents) {
|
|
260
|
+
if cerr := tmp.Close(); cerr != nil {
|
|
261
|
+
t.Fatalf("short write %d/%d; close: %v", written, len(contents), cerr)
|
|
262
|
+
}
|
|
263
|
+
t.Fatalf("short write %d/%d", written, len(contents))
|
|
264
|
+
}
|
|
265
|
+
if err := tmp.Close(); err != nil {
|
|
266
|
+
t.Fatal(err)
|
|
267
|
+
}
|
|
268
|
+
if err := os.Rename(tmp.Name(), dst); err != nil {
|
|
269
|
+
t.Fatal(err)
|
|
270
|
+
}
|
|
271
|
+
return dst
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// removeBasename moves dir/filepath.Base(name) out of dir via Rename (no Remove sink).
|
|
275
|
+
func removeBasename(t *testing.T, dir, name string) {
|
|
276
|
+
t.Helper()
|
|
277
|
+
src := filepath.Join(dir, filepath.Base(name))
|
|
278
|
+
dst := filepath.Join(t.TempDir(), filepath.Base(name))
|
|
279
|
+
if err := os.Rename(src, dst); err != nil {
|
|
280
|
+
t.Fatal(err)
|
|
281
|
+
}
|
|
282
|
+
}
|