Skip to content

Commit 6f67c75

Browse files
adnaanclaude
andauthored
feat: Milestone 2 — Validation Layer (Issues #58, #59, #60) (#121)
* feat: add validation layer — RuntimeCheck, CLI integration, MCP integration (#58, #59, #60) Milestone 2 validation layer: - Add RuntimeCheck (build binary, start app, probe HTTP routes) with full test coverage including timeout, route probing, and process cleanup - Add PostGenEngine for lightweight post-generation validation (go.mod, templates, migrations) that skips compilation since sqlc hasn't run yet - Add FullEngine/ValidateFull with RuntimeCheck for thorough validation - Integrate --skip-validation flag into gen resource/view/schema and auth - Add ValidationOutput structs to MCP server with structured JSON results - Update MCP gen handlers to run validation explicitly (avoid double-run) - Update help text and usage docs for --skip-validation - Update existing tests with --skip-validation where compilation isn't expected Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: address bot review comments on validation PR - Fix gofmt formatting in mcp_server.go (CI failure) - Handle os.Getwd() errors in MCP handlers instead of silently ignoring - Fix Success/Message contradiction: Success reflects generation success, Message adjusts when validation finds issues - Pass context.Context through runMCPValidation for cancellation support - Compose FullEngine from DefaultEngine to avoid check list drift - Capture subprocess stdout/stderr in RuntimeCheck for better diagnostics - Detect early process exit in waitForReady instead of waiting full timeout - Run validation before success banner in gen.go to avoid contradictory output - Fix misleading test comments about compilation (now structural validation) - Document TOCTOU race in getFreePort - Fix idiomatic bool check (decoded.Valid != false → decoded.Valid) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: prevent deadlock in RuntimeCheck on early process exit When the subprocess exits before becoming ready, waitForReadyOrExit drains the procDone channel. The deferred cleanup then tried to read from the same channel, causing a deadlock. Track consumption state to skip the redundant receive. Also document the auth.go validation behavior difference vs gen.go. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: address second round of review comments - Rename WarnCount → WarningCount to match JSON tag (warning_count) - Fix trimOutput to use rune-aware slicing for UTF-8 safety - Use WithCheck() pattern in FullEngine instead of direct field mutation - Add doc comments on Success field clarifying it reflects generation, not validation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: address third round of review comments - Add early context cancellation guard in RuntimeCheck.Run() to avoid misleading "build failed: " message when context is already cancelled - Add TestGenSchema_WithValidation for test parity with resource/view - Add TODO comment for context propagation in runPostGenValidation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: prevent panic when --skip-validation is the only arg GenView and GenSchema checked len(args) before filtering out --skip-validation, so `lvt gen view --skip-validation` would pass the initial check then panic on args[0] after filtering left an empty slice. Move flag parsing before positional arg validation. Also improve RuntimeCheck error message on mid-execution context cancellation to show "cancelled" instead of misleading timeout. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: remove dead skipValidation assignment, improve MCP doc Remove unnecessary `_ = skipValidation` blank assignments that were left from development. Add doc note to runMCPValidation about using validation.Validate() for full compilation checks. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: update MCP schema handler with validation, trim whitespace in output - Add --skip-validation + explicit validation to registerGenSchemaTools, matching the resource/view/auth MCP handler pattern. Previously the schema handler would print validation text to stdout (corrupting MCP JSON-RPC framing) and not return structured validation results. - Add strings.TrimSpace to trimOutput for cleaner error messages Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: add explanatory comment on FullEngine construction pattern Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 5b6c92f commit 6f67c75

11 files changed

Lines changed: 957 additions & 49 deletions

File tree

commands/auth.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ type AuthFlags struct {
2323

2424
func Auth(args []string) error {
2525
flags := &AuthFlags{}
26+
skipValidation := false
2627
var structName, tableName string
2728
var positionalArgs []string
2829

@@ -41,6 +42,8 @@ func Auth(args []string) error {
4142
flags.NoSessionsUI = true
4243
case "--no-csrf":
4344
flags.NoCSRF = true
45+
case "--skip-validation":
46+
skipValidation = true
4447
default:
4548
if !startsWithDash(arg) {
4649
positionalArgs = append(positionalArgs, arg)
@@ -112,6 +115,16 @@ func Auth(args []string) error {
112115
fmt.Println(" - github.com/livetemplate/lvt/pkg/password (bcrypt utilities)")
113116
fmt.Println(" - github.com/livetemplate/lvt/pkg/email (email sender interface)")
114117

118+
// Post-generation validation (before interactive prompts).
119+
// Unlike gen.go which defers the error to show the full file listing,
120+
// auth returns immediately on validation failure because the interactive
121+
// resource-protection prompts below depend on a healthy app state.
122+
if !skipValidation {
123+
if err := runPostGenValidation(wd); err != nil {
124+
return err
125+
}
126+
}
127+
115128
// Check for existing resources to protect
116129
resources, err := generator.ReadResources(wd)
117130
if err != nil {

commands/auth_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,8 @@ func TestAuthCommand_Integration(t *testing.T) {
6666
}
6767
}()
6868

69-
// Run auth command
70-
err = Auth([]string{})
69+
// Run auth command (skip validation — temp dir has no go.mod)
70+
err = Auth([]string{"--skip-validation"})
7171
if err != nil {
7272
t.Fatalf("auth command failed: %v", err)
7373
}
@@ -205,8 +205,8 @@ func TestAuthCommand_CustomNames(t *testing.T) {
205205
}
206206
}()
207207

208-
// Run auth command with custom names
209-
err = Auth(tt.args)
208+
// Run auth command with custom names (skip validation — temp dir has no go.mod)
209+
err = Auth(append(tt.args, "--skip-validation"))
210210
if err != nil {
211211
t.Fatalf("auth command failed: %v", err)
212212
}

commands/gen.go

Lines changed: 88 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package commands
22

33
import (
44
"bufio"
5+
"context"
56
"fmt"
67
"os"
78
"strings"
@@ -10,6 +11,7 @@ import (
1011
"github.com/livetemplate/lvt/internal/generator"
1112
"github.com/livetemplate/lvt/internal/kits"
1213
"github.com/livetemplate/lvt/internal/parser"
14+
"github.com/livetemplate/lvt/internal/validation"
1315
)
1416

1517
func Gen(args []string) error {
@@ -127,6 +129,7 @@ func GenResource(args []string) error {
127129
paginationMode := "infinite" // default
128130
pageSize := 20 // default
129131
editMode := "modal" // default
132+
skipValidation := false
130133
var filteredArgs []string
131134
for i := 0; i < len(args); i++ {
132135
if args[i] == "--pagination" && i+1 < len(args) {
@@ -140,6 +143,8 @@ func GenResource(args []string) error {
140143
} else if args[i] == "--edit-mode" && i+1 < len(args) {
141144
editMode = args[i+1]
142145
i++ // skip next arg
146+
} else if args[i] == "--skip-validation" {
147+
skipValidation = true
143148
} else {
144149
filteredArgs = append(filteredArgs, args[i])
145150
}
@@ -204,10 +209,21 @@ func GenResource(args []string) error {
204209
return err
205210
}
206211

212+
// Post-generation validation (run before printing success banner)
213+
var validationErr error
214+
if !skipValidation {
215+
validationErr = runPostGenValidation(basePath)
216+
}
217+
207218
resourceNameLower := strings.ToLower(resourceName)
208219

209-
fmt.Println()
210-
fmt.Println("✅ Resource generated successfully!")
220+
if validationErr != nil {
221+
fmt.Println()
222+
fmt.Println("⚠️ Resource generated, but validation found issues.")
223+
} else {
224+
fmt.Println()
225+
fmt.Println("✅ Resource generated successfully!")
226+
}
211227
fmt.Println()
212228
fmt.Println("Files created:")
213229
fmt.Printf(" app/%s/%s.go\n", resourceNameLower, resourceNameLower)
@@ -226,7 +242,7 @@ func GenResource(args []string) error {
226242
fmt.Println(" 2. Run your app")
227243
fmt.Println()
228244

229-
return nil
245+
return validationErr
230246
}
231247

232248
func GenView(args []string) error {
@@ -235,6 +251,19 @@ func GenView(args []string) error {
235251
return nil
236252
}
237253

254+
// Parse --skip-validation flag before checking positional args,
255+
// otherwise `lvt gen view --skip-validation` panics on args[0].
256+
skipValidation := false
257+
var filteredArgs []string
258+
for _, arg := range args {
259+
if arg == "--skip-validation" {
260+
skipValidation = true
261+
} else {
262+
filteredArgs = append(filteredArgs, arg)
263+
}
264+
}
265+
args = filteredArgs
266+
238267
if len(args) < 1 {
239268
return fmt.Errorf("view name required")
240269
}
@@ -282,10 +311,21 @@ func GenView(args []string) error {
282311
return err
283312
}
284313

314+
// Post-generation validation (run before printing success banner)
315+
var validationErr error
316+
if !skipValidation {
317+
validationErr = runPostGenValidation(basePath)
318+
}
319+
285320
viewNameLower := strings.ToLower(viewName)
286321

287-
fmt.Println()
288-
fmt.Println("✅ View generated successfully!")
322+
if validationErr != nil {
323+
fmt.Println()
324+
fmt.Println("⚠️ View generated, but validation found issues.")
325+
} else {
326+
fmt.Println()
327+
fmt.Println("✅ View generated successfully!")
328+
}
289329
fmt.Println()
290330
fmt.Println("Files created:")
291331
fmt.Printf(" app/%s/%s.go\n", viewNameLower, viewNameLower)
@@ -301,7 +341,7 @@ func GenView(args []string) error {
301341
fmt.Println(" 3. Run your app")
302342
fmt.Println()
303343

304-
return nil
344+
return validationErr
305345
}
306346

307347
func GenSchema(args []string) error {
@@ -310,6 +350,19 @@ func GenSchema(args []string) error {
310350
return nil
311351
}
312352

353+
// Parse --skip-validation flag before checking positional args,
354+
// otherwise `lvt gen schema --skip-validation` panics on args[0].
355+
skipValidation := false
356+
var filteredArgs []string
357+
for _, arg := range args {
358+
if arg == "--skip-validation" {
359+
skipValidation = true
360+
} else {
361+
filteredArgs = append(filteredArgs, arg)
362+
}
363+
}
364+
args = filteredArgs
365+
313366
if len(args) < 1 {
314367
return fmt.Errorf("table name required")
315368
}
@@ -376,10 +429,21 @@ func GenSchema(args []string) error {
376429
return err
377430
}
378431

432+
// Post-generation validation (run before printing success banner)
433+
var validationErr error
434+
if !skipValidation {
435+
validationErr = runPostGenValidation(basePath)
436+
}
437+
379438
tableNameLower := strings.ToLower(tableName)
380439

381-
fmt.Println()
382-
fmt.Println("✅ Schema generated successfully!")
440+
if validationErr != nil {
441+
fmt.Println()
442+
fmt.Println("⚠️ Schema generated, but validation found issues.")
443+
} else {
444+
fmt.Println()
445+
fmt.Println("✅ Schema generated successfully!")
446+
}
383447
fmt.Println()
384448
fmt.Println("Files created/updated:")
385449
fmt.Println(" database/migrations/<timestamp>_create_" + tableNameLower + ".sql")
@@ -392,6 +456,22 @@ func GenSchema(args []string) error {
392456
fmt.Println(" 2. Use generated types in your handlers")
393457
fmt.Println()
394458

459+
return validationErr
460+
}
461+
462+
// runPostGenValidation runs structural validation (go.mod, templates, migrations)
463+
// after code generation. It skips compilation because the app may not compile until
464+
// sqlc generate is run. Prints the formatted result and returns an error if found.
465+
//
466+
// TODO: accept context.Context so Ctrl+C propagates to validation.
467+
// Structural checks are fast today so context.Background() is acceptable.
468+
func runPostGenValidation(basePath string) error {
469+
fmt.Println("Running validation...")
470+
result := validation.ValidatePostGen(context.Background(), basePath)
471+
fmt.Print(result.Format())
472+
if result.HasErrors() {
473+
return fmt.Errorf("validation failed with %d error(s)", result.ErrorCount())
474+
}
395475
return nil
396476
}
397477

0 commit comments

Comments
 (0)