Skip to content

Commit 448b1ef

Browse files
julianknutsenclaude
andcommitted
Add integration tests for hop/wl-commons regression gate
Clone the live commons database once via TestMain, then verify schema (tables, columns) and data invariants (valid statuses, priorities, types, non-empty wanted board, schema version). CI runs integration job on push to main only, gated behind the check job. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 3a414dc commit 448b1ef

File tree

3 files changed

+327
-0
lines changed

3 files changed

+327
-0
lines changed

.github/workflows/ci.yml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,21 @@ jobs:
4040
files: coverage.txt
4141
token: ${{ secrets.CODECOV_TOKEN }}
4242
verbose: true
43+
44+
integration:
45+
name: Integration
46+
needs: check
47+
if: github.event_name == 'push'
48+
runs-on: ubuntu-latest
49+
steps:
50+
- uses: actions/checkout@v6
51+
52+
- uses: actions/setup-go@v6
53+
with:
54+
go-version-file: go.mod
55+
56+
- name: Install dolt
57+
run: sudo bash -c 'curl -L https://github.com/dolthub/dolt/releases/latest/download/install.sh | bash'
58+
59+
- name: Integration tests
60+
run: make test-integration

TESTING.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ Run with: `go test -tags integration ./test/`
4545
| Does SQL escaping prevent injection? | Unit test |
4646
| Does the federation join workflow call steps in order? | Unit test |
4747
| Does a real dolt clone succeed from DoltHub? | Integration |
48+
| Does `hop/wl-commons` schema match expected tables/columns? | Integration |
49+
| Are all `wanted` statuses/priorities/types valid? | Integration |
4850

4951
## Test doubles
5052

test/integration/commons_test.go

Lines changed: 307 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,307 @@
1+
//go:build integration
2+
3+
package integration
4+
5+
import (
6+
"encoding/csv"
7+
"fmt"
8+
"os"
9+
"os/exec"
10+
"path/filepath"
11+
"strconv"
12+
"strings"
13+
"testing"
14+
)
15+
16+
// cloneDir is set once in TestMain; all tests query this shared clone.
17+
var cloneDir string
18+
19+
func TestMain(m *testing.M) {
20+
doltPath, err := exec.LookPath("dolt")
21+
if err != nil {
22+
fmt.Fprintf(os.Stderr, "dolt not found in PATH — skipping integration tests\n")
23+
os.Exit(1)
24+
}
25+
26+
tmp, err := os.MkdirTemp("", "wl-integration-*")
27+
if err != nil {
28+
fmt.Fprintf(os.Stderr, "creating temp dir: %v\n", err)
29+
os.Exit(1)
30+
}
31+
32+
cloneDir = filepath.Join(tmp, "wl-commons")
33+
34+
cmd := exec.Command(doltPath, "clone", "hop/wl-commons", cloneDir)
35+
cmd.Stderr = os.Stderr
36+
if err := cmd.Run(); err != nil {
37+
fmt.Fprintf(os.Stderr, "cloning hop/wl-commons: %v\n", err)
38+
os.RemoveAll(tmp)
39+
os.Exit(1)
40+
}
41+
42+
code := m.Run()
43+
os.RemoveAll(tmp)
44+
os.Exit(code)
45+
}
46+
47+
// doltQuery runs a SQL query against the cloned database and returns CSV output.
48+
func doltQuery(query string) (string, error) {
49+
doltPath, err := exec.LookPath("dolt")
50+
if err != nil {
51+
return "", err
52+
}
53+
cmd := exec.Command(doltPath, "sql", "-q", query, "-r", "csv")
54+
cmd.Dir = cloneDir
55+
out, err := cmd.Output()
56+
if err != nil {
57+
if exitErr, ok := err.(*exec.ExitError); ok {
58+
return "", fmt.Errorf("query failed: %s", string(exitErr.Stderr))
59+
}
60+
return "", err
61+
}
62+
return string(out), nil
63+
}
64+
65+
// parseCSVRows parses CSV output, returning header + data rows.
66+
func parseCSVRows(raw string) ([][]string, error) {
67+
r := csv.NewReader(strings.NewReader(strings.TrimSpace(raw)))
68+
return r.ReadAll()
69+
}
70+
71+
func TestCommonsClone(t *testing.T) {
72+
dotDolt := filepath.Join(cloneDir, ".dolt")
73+
info, err := os.Stat(dotDolt)
74+
if err != nil {
75+
t.Fatalf(".dolt directory not found in clone: %v", err)
76+
}
77+
if !info.IsDir() {
78+
t.Fatalf(".dolt exists but is not a directory")
79+
}
80+
}
81+
82+
func TestCommonsSchema_Tables(t *testing.T) {
83+
out, err := doltQuery("SHOW TABLES")
84+
if err != nil {
85+
t.Fatalf("SHOW TABLES: %v", err)
86+
}
87+
88+
rows, err := parseCSVRows(out)
89+
if err != nil {
90+
t.Fatalf("parsing CSV: %v", err)
91+
}
92+
93+
tables := make(map[string]bool)
94+
for _, row := range rows[1:] { // skip header
95+
if len(row) > 0 {
96+
tables[row[0]] = true
97+
}
98+
}
99+
100+
expected := []string{"_meta", "rigs", "wanted", "completions", "stamps", "badges", "chain_meta"}
101+
for _, name := range expected {
102+
if !tables[name] {
103+
t.Errorf("missing table %q; got tables: %v", name, tables)
104+
}
105+
}
106+
}
107+
108+
func TestCommonsSchema_WantedColumns(t *testing.T) {
109+
out, err := doltQuery("DESCRIBE wanted")
110+
if err != nil {
111+
t.Fatalf("DESCRIBE wanted: %v", err)
112+
}
113+
114+
rows, err := parseCSVRows(out)
115+
if err != nil {
116+
t.Fatalf("parsing CSV: %v", err)
117+
}
118+
119+
columns := make(map[string]bool)
120+
for _, row := range rows[1:] {
121+
if len(row) > 0 {
122+
columns[row[0]] = true
123+
}
124+
}
125+
126+
expected := []string{
127+
"id", "title", "description", "project", "type", "priority",
128+
"tags", "posted_by", "claimed_by", "status", "effort_level",
129+
"evidence_url", "sandbox_required", "sandbox_scope", "sandbox_min_tier",
130+
"created_at", "updated_at",
131+
}
132+
for _, col := range expected {
133+
if !columns[col] {
134+
t.Errorf("wanted table missing column %q", col)
135+
}
136+
}
137+
}
138+
139+
func TestCommonsSchema_RigsColumns(t *testing.T) {
140+
out, err := doltQuery("DESCRIBE rigs")
141+
if err != nil {
142+
t.Fatalf("DESCRIBE rigs: %v", err)
143+
}
144+
145+
rows, err := parseCSVRows(out)
146+
if err != nil {
147+
t.Fatalf("parsing CSV: %v", err)
148+
}
149+
150+
columns := make(map[string]bool)
151+
for _, row := range rows[1:] {
152+
if len(row) > 0 {
153+
columns[row[0]] = true
154+
}
155+
}
156+
157+
expected := []string{"handle", "display_name", "dolthub_org"}
158+
for _, col := range expected {
159+
if !columns[col] {
160+
t.Errorf("rigs table missing column %q", col)
161+
}
162+
}
163+
}
164+
165+
func TestCommonsData_WantedNotEmpty(t *testing.T) {
166+
out, err := doltQuery("SELECT COUNT(*) AS cnt FROM wanted")
167+
if err != nil {
168+
t.Fatalf("counting wanted rows: %v", err)
169+
}
170+
171+
rows, err := parseCSVRows(out)
172+
if err != nil {
173+
t.Fatalf("parsing CSV: %v", err)
174+
}
175+
if len(rows) < 2 {
176+
t.Fatal("no rows returned from COUNT query")
177+
}
178+
179+
count, err := strconv.Atoi(rows[1][0])
180+
if err != nil {
181+
t.Fatalf("parsing count %q: %v", rows[1][0], err)
182+
}
183+
if count == 0 {
184+
t.Error("wanted table is empty — expected at least 1 row")
185+
}
186+
}
187+
188+
func TestCommonsData_ValidStatuses(t *testing.T) {
189+
out, err := doltQuery("SELECT DISTINCT status FROM wanted")
190+
if err != nil {
191+
t.Fatalf("querying statuses: %v", err)
192+
}
193+
194+
rows, err := parseCSVRows(out)
195+
if err != nil {
196+
t.Fatalf("parsing CSV: %v", err)
197+
}
198+
199+
valid := map[string]bool{
200+
"open": true, "claimed": true, "in_review": true,
201+
"completed": true, "withdrawn": true, "validated": true,
202+
}
203+
for _, row := range rows[1:] {
204+
if len(row) == 0 {
205+
continue
206+
}
207+
s := row[0]
208+
if !valid[s] {
209+
t.Errorf("invalid status %q — expected one of %v", s, valid)
210+
}
211+
}
212+
}
213+
214+
func TestCommonsData_ValidPriorities(t *testing.T) {
215+
out, err := doltQuery("SELECT DISTINCT priority FROM wanted WHERE priority IS NOT NULL")
216+
if err != nil {
217+
t.Fatalf("querying priorities: %v", err)
218+
}
219+
220+
rows, err := parseCSVRows(out)
221+
if err != nil {
222+
t.Fatalf("parsing CSV: %v", err)
223+
}
224+
225+
for _, row := range rows[1:] {
226+
if len(row) == 0 {
227+
continue
228+
}
229+
p, err := strconv.Atoi(row[0])
230+
if err != nil {
231+
t.Errorf("non-integer priority %q: %v", row[0], err)
232+
continue
233+
}
234+
if p < 0 || p > 4 {
235+
t.Errorf("priority %d out of range 0–4", p)
236+
}
237+
}
238+
}
239+
240+
func TestCommonsData_ValidTypes(t *testing.T) {
241+
out, err := doltQuery("SELECT DISTINCT type FROM wanted WHERE type IS NOT NULL AND type != ''")
242+
if err != nil {
243+
t.Fatalf("querying types: %v", err)
244+
}
245+
246+
rows, err := parseCSVRows(out)
247+
if err != nil {
248+
t.Fatalf("parsing CSV: %v", err)
249+
}
250+
251+
valid := map[string]bool{
252+
"feature": true, "bug": true, "design": true,
253+
"rfc": true, "docs": true, "research": true, "community": true,
254+
}
255+
for _, row := range rows[1:] {
256+
if len(row) == 0 {
257+
continue
258+
}
259+
typ := row[0]
260+
if !valid[typ] {
261+
t.Errorf("invalid type %q — expected one of %v", typ, valid)
262+
}
263+
}
264+
}
265+
266+
func TestCommonsData_OpenItemsExist(t *testing.T) {
267+
out, err := doltQuery("SELECT COUNT(*) AS cnt FROM wanted WHERE status = 'open'")
268+
if err != nil {
269+
t.Fatalf("counting open items: %v", err)
270+
}
271+
272+
rows, err := parseCSVRows(out)
273+
if err != nil {
274+
t.Fatalf("parsing CSV: %v", err)
275+
}
276+
if len(rows) < 2 {
277+
t.Fatal("no rows returned from COUNT query")
278+
}
279+
280+
count, err := strconv.Atoi(rows[1][0])
281+
if err != nil {
282+
t.Fatalf("parsing count %q: %v", rows[1][0], err)
283+
}
284+
if count == 0 {
285+
t.Error("no open wanted items — expected at least 1")
286+
}
287+
}
288+
289+
func TestCommonsData_MetaVersion(t *testing.T) {
290+
out, err := doltQuery("SELECT `value` FROM _meta WHERE `key` = 'schema_version'")
291+
if err != nil {
292+
t.Fatalf("querying _meta schema_version: %v", err)
293+
}
294+
295+
rows, err := parseCSVRows(out)
296+
if err != nil {
297+
t.Fatalf("parsing CSV: %v", err)
298+
}
299+
if len(rows) < 2 {
300+
t.Fatal("no schema_version row in _meta")
301+
}
302+
303+
version := rows[1][0]
304+
if version != "1.1" {
305+
t.Errorf("schema_version = %q, want %q", version, "1.1")
306+
}
307+
}

0 commit comments

Comments
 (0)