T04: Checker — verifikacija posle agenta
- Verify() pokreće build, vet, test sa merenjem trajanja - Ako build padne, ostalo se preskače - parseTestCount parsira go test -v output - FormatResult za čitljiv ispis - 10 checker testova — svi prolaze Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
79bcd52076
commit
5d869f56ce
46
TASKS/reports/T04-report.md
Normal file
46
TASKS/reports/T04-report.md
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
# T04 Izveštaj: Checker — verifikacija posle agenta
|
||||||
|
|
||||||
|
**Agent:** coder
|
||||||
|
**Model:** Opus
|
||||||
|
**Datum:** 2026-02-20
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Šta je urađeno
|
||||||
|
|
||||||
|
Implementiran checker u `code/internal/supervisor/`:
|
||||||
|
|
||||||
|
### Kreirani fajlovi
|
||||||
|
|
||||||
|
| Fajl | Opis |
|
||||||
|
|------|------|
|
||||||
|
| `checker.go` | Verify, runCheck, parseTestCount, FormatResult |
|
||||||
|
| `checker_test.go` | 10 testova sa privremenim Go projektima |
|
||||||
|
|
||||||
|
### Funkcije
|
||||||
|
|
||||||
|
- **Verify(projectPath)** — pokreće build, vet, test; ako build padne, ostalo se preskače
|
||||||
|
- **runCheck** — pokreće jednu komandu, meri trajanje, hvata output
|
||||||
|
- **parseTestCount** — broji test rezultate iz go test -v outputa
|
||||||
|
- **FormatResult** — formatira VerifyResult za čitljiv ispis
|
||||||
|
|
||||||
|
### Testovi — 10/10 PASS (checker)
|
||||||
|
|
||||||
|
```
|
||||||
|
TestVerify_PassingProject PASS (0.51s)
|
||||||
|
TestVerify_BuildFail PASS (0.03s)
|
||||||
|
TestVerify_TestFail PASS (0.43s)
|
||||||
|
TestVerify_Duration PASS (0.25s)
|
||||||
|
TestVerify_OutputPopulated PASS (0.47s)
|
||||||
|
TestParseTestCount_PassingTests PASS
|
||||||
|
TestParseTestCount_MixedResults PASS
|
||||||
|
TestParseTestCount_NoTests PASS
|
||||||
|
TestParseTestCount_Empty PASS
|
||||||
|
TestFormatResult PASS
|
||||||
|
```
|
||||||
|
|
||||||
|
### Ukupno projekat: 33 testa, svi prolaze
|
||||||
|
|
||||||
|
- `go vet ./...` — čist
|
||||||
|
- `go build ./...` — prolazi
|
||||||
|
- `make all` — prolazi
|
||||||
82
TASKS/review/T04.md
Normal file
82
TASKS/review/T04.md
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
# T04: Checker — verifikacija posle agenta
|
||||||
|
|
||||||
|
**Kreirao:** planer
|
||||||
|
**Datum:** 2026-02-20
|
||||||
|
**Agent:** coder
|
||||||
|
**Model:** Sonnet
|
||||||
|
**Zavisi od:** T01 ✅
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Opis
|
||||||
|
|
||||||
|
Posle svake Claude Code sesije, nezavisno pokrene build + test + vet.
|
||||||
|
Parsira output, broji testove, meri vreme svake provere.
|
||||||
|
|
||||||
|
## Fajlovi za kreiranje
|
||||||
|
|
||||||
|
```
|
||||||
|
code/internal/supervisor/
|
||||||
|
├── checker.go ← Verify(), CheckResult, VerifyResult
|
||||||
|
└── checker_test.go ← testovi sa privremenim Go projektom
|
||||||
|
```
|
||||||
|
|
||||||
|
## Strukture
|
||||||
|
|
||||||
|
```go
|
||||||
|
type CheckResult struct {
|
||||||
|
Name string // "build", "test", "vet"
|
||||||
|
Status string // "pass", "fail"
|
||||||
|
Output string // stdout + stderr
|
||||||
|
Duration time.Duration
|
||||||
|
TestCount int // samo za test (parsira "ok ... N tests")
|
||||||
|
}
|
||||||
|
|
||||||
|
type VerifyResult struct {
|
||||||
|
Build CheckResult
|
||||||
|
Vet CheckResult
|
||||||
|
Test CheckResult
|
||||||
|
AllPassed bool
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Funkcije
|
||||||
|
|
||||||
|
- `Verify(projectPath string) VerifyResult` — pokrene sva tri checka
|
||||||
|
- `runCheck(name, command, projectPath string) CheckResult` — pokrene jednu komandu
|
||||||
|
- `parseTestCount(output string) int` — parsira broj testova iz go test outputa
|
||||||
|
|
||||||
|
## Komande koje pokreće
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go build ./...
|
||||||
|
go vet ./...
|
||||||
|
go test ./... -count=1 -v
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testovi
|
||||||
|
|
||||||
|
- Napravi temp Go projekat koji prolazi → Verify vraća AllPassed=true
|
||||||
|
- Napravi temp Go projekat sa syntax error → Build fail, ostalo preskočeno
|
||||||
|
- Napravi temp Go projekat sa failing testom → Test fail
|
||||||
|
- parseTestCount: "ok github.com/dal/kaos 0.003s" → parsira tačno
|
||||||
|
- parseTestCount: "FAIL" → 0
|
||||||
|
- Proveri da Duration > 0 za svaki check
|
||||||
|
- Proveri da se Output popunjava
|
||||||
|
|
||||||
|
## Očekivani izlaz
|
||||||
|
|
||||||
|
Verify pokrene tri komande, parsira svaku, vrati VerifyResult.
|
||||||
|
`go test ./internal/supervisor/ -v` — svi testovi zeleni.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Pitanja
|
||||||
|
|
||||||
|
*(agent piše pitanja ovde, planer odgovara)*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Odgovori
|
||||||
|
|
||||||
|
*(planer piše odgovore ovde)*
|
||||||
113
code/internal/supervisor/checker.go
Normal file
113
code/internal/supervisor/checker.go
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
package supervisor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os/exec"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CheckResult holds the result of a single verification check.
|
||||||
|
type CheckResult struct {
|
||||||
|
Name string // "build", "test", "vet"
|
||||||
|
Status string // "pass", "fail"
|
||||||
|
Output string // stdout + stderr
|
||||||
|
Duration time.Duration
|
||||||
|
TestCount int // only for test (parsed from output)
|
||||||
|
}
|
||||||
|
|
||||||
|
// VerifyResult holds the combined result of all verification checks.
|
||||||
|
type VerifyResult struct {
|
||||||
|
Build CheckResult
|
||||||
|
Vet CheckResult
|
||||||
|
Test CheckResult
|
||||||
|
AllPassed bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify runs build, vet, and test checks on the given Go project path.
|
||||||
|
// If build fails, vet and test are skipped.
|
||||||
|
func Verify(projectPath string) VerifyResult {
|
||||||
|
var result VerifyResult
|
||||||
|
|
||||||
|
result.Build = runCheck("build", "go build ./...", projectPath)
|
||||||
|
if result.Build.Status == "fail" {
|
||||||
|
result.Vet = CheckResult{Name: "vet", Status: "fail", Output: "skipped: build failed"}
|
||||||
|
result.Test = CheckResult{Name: "test", Status: "fail", Output: "skipped: build failed"}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Vet = runCheck("vet", "go vet ./...", projectPath)
|
||||||
|
|
||||||
|
result.Test = runCheck("test", "go test ./... -count=1 -v", projectPath)
|
||||||
|
result.Test.TestCount = parseTestCount(result.Test.Output)
|
||||||
|
|
||||||
|
result.AllPassed = result.Build.Status == "pass" &&
|
||||||
|
result.Vet.Status == "pass" &&
|
||||||
|
result.Test.Status == "pass"
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// runCheck executes a single shell command in the given directory and returns
|
||||||
|
// a CheckResult with the output, status, and duration.
|
||||||
|
func runCheck(name, command, projectPath string) CheckResult {
|
||||||
|
start := time.Now()
|
||||||
|
|
||||||
|
parts := strings.Fields(command)
|
||||||
|
cmd := exec.Command(parts[0], parts[1:]...)
|
||||||
|
cmd.Dir = projectPath
|
||||||
|
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
duration := time.Since(start)
|
||||||
|
|
||||||
|
status := "pass"
|
||||||
|
if err != nil {
|
||||||
|
status = "fail"
|
||||||
|
}
|
||||||
|
|
||||||
|
return CheckResult{
|
||||||
|
Name: name,
|
||||||
|
Status: status,
|
||||||
|
Output: string(output),
|
||||||
|
Duration: duration,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// testCountRegex matches lines like:
|
||||||
|
//
|
||||||
|
// ok github.com/dal/kaos/internal/config 0.003s
|
||||||
|
// ok github.com/dal/kaos/internal/supervisor 0.008s
|
||||||
|
var testCountRegex = regexp.MustCompile(`(?m)^---\s+(PASS|FAIL):\s+`)
|
||||||
|
|
||||||
|
// passLineRegex matches "ok" lines from go test output.
|
||||||
|
var passLineRegex = regexp.MustCompile(`(?m)^ok\s+\S+\s+[\d.]+s`)
|
||||||
|
|
||||||
|
// parseTestCount counts the number of individual test results in go test -v output.
|
||||||
|
// It counts lines matching "--- PASS:" or "--- FAIL:".
|
||||||
|
func parseTestCount(output string) int {
|
||||||
|
matches := testCountRegex.FindAllString(output, -1)
|
||||||
|
return len(matches)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FormatResult returns a human-readable summary of the verification.
|
||||||
|
func FormatResult(r VerifyResult) string {
|
||||||
|
var b strings.Builder
|
||||||
|
for _, c := range []CheckResult{r.Build, r.Vet, r.Test} {
|
||||||
|
icon := "✅"
|
||||||
|
if c.Status == "fail" {
|
||||||
|
icon = "❌"
|
||||||
|
}
|
||||||
|
line := fmt.Sprintf("%s %s: %s (%s)", icon, c.Name, c.Status, c.Duration.Round(time.Millisecond))
|
||||||
|
if c.Name == "test" && c.TestCount > 0 {
|
||||||
|
line += fmt.Sprintf(" [%d tests]", c.TestCount)
|
||||||
|
}
|
||||||
|
b.WriteString(line + "\n")
|
||||||
|
}
|
||||||
|
if r.AllPassed {
|
||||||
|
b.WriteString("\nAll checks passed.")
|
||||||
|
} else {
|
||||||
|
b.WriteString("\nSome checks failed.")
|
||||||
|
}
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
263
code/internal/supervisor/checker_test.go
Normal file
263
code/internal/supervisor/checker_test.go
Normal file
@ -0,0 +1,263 @@
|
|||||||
|
package supervisor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// setupTempGoProject creates a minimal Go project in a temp directory.
|
||||||
|
func setupTempGoProject(t *testing.T, mainContent, testContent string) string {
|
||||||
|
t.Helper()
|
||||||
|
dir := t.TempDir()
|
||||||
|
|
||||||
|
// go.mod
|
||||||
|
goMod := "module tempproject\n\ngo 1.22\n"
|
||||||
|
if err := os.WriteFile(filepath.Join(dir, "go.mod"), []byte(goMod), 0644); err != nil {
|
||||||
|
t.Fatalf("write go.mod: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// main.go
|
||||||
|
if err := os.WriteFile(filepath.Join(dir, "main.go"), []byte(mainContent), 0644); err != nil {
|
||||||
|
t.Fatalf("write main.go: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// test file (optional)
|
||||||
|
if testContent != "" {
|
||||||
|
if err := os.WriteFile(filepath.Join(dir, "main_test.go"), []byte(testContent), 0644); err != nil {
|
||||||
|
t.Fatalf("write main_test.go: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return dir
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVerify_PassingProject(t *testing.T) {
|
||||||
|
mainGo := `package main
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
fmt.Println("hello")
|
||||||
|
}
|
||||||
|
|
||||||
|
func Add(a, b int) int {
|
||||||
|
return a + b
|
||||||
|
}
|
||||||
|
`
|
||||||
|
testGo := `package main
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestAdd(t *testing.T) {
|
||||||
|
if Add(1, 2) != 3 {
|
||||||
|
t.Fatal("expected 3")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAddZero(t *testing.T) {
|
||||||
|
if Add(0, 0) != 0 {
|
||||||
|
t.Fatal("expected 0")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
dir := setupTempGoProject(t, mainGo, testGo)
|
||||||
|
|
||||||
|
result := Verify(dir)
|
||||||
|
|
||||||
|
if !result.AllPassed {
|
||||||
|
t.Errorf("expected AllPassed=true\nBuild: %s %s\nVet: %s %s\nTest: %s %s",
|
||||||
|
result.Build.Status, result.Build.Output,
|
||||||
|
result.Vet.Status, result.Vet.Output,
|
||||||
|
result.Test.Status, result.Test.Output)
|
||||||
|
}
|
||||||
|
|
||||||
|
if result.Build.Status != "pass" {
|
||||||
|
t.Errorf("expected build pass, got %s", result.Build.Status)
|
||||||
|
}
|
||||||
|
if result.Vet.Status != "pass" {
|
||||||
|
t.Errorf("expected vet pass, got %s", result.Vet.Status)
|
||||||
|
}
|
||||||
|
if result.Test.Status != "pass" {
|
||||||
|
t.Errorf("expected test pass, got %s", result.Test.Status)
|
||||||
|
}
|
||||||
|
if result.Test.TestCount != 2 {
|
||||||
|
t.Errorf("expected 2 tests, got %d", result.Test.TestCount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVerify_BuildFail(t *testing.T) {
|
||||||
|
badGo := `package main
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
this is not valid go code
|
||||||
|
}
|
||||||
|
`
|
||||||
|
dir := setupTempGoProject(t, badGo, "")
|
||||||
|
|
||||||
|
result := Verify(dir)
|
||||||
|
|
||||||
|
if result.AllPassed {
|
||||||
|
t.Error("expected AllPassed=false")
|
||||||
|
}
|
||||||
|
if result.Build.Status != "fail" {
|
||||||
|
t.Errorf("expected build fail, got %s", result.Build.Status)
|
||||||
|
}
|
||||||
|
// Vet and test should be skipped
|
||||||
|
if result.Vet.Status != "fail" {
|
||||||
|
t.Errorf("expected vet fail (skipped), got %s", result.Vet.Status)
|
||||||
|
}
|
||||||
|
if result.Test.Status != "fail" {
|
||||||
|
t.Errorf("expected test fail (skipped), got %s", result.Test.Status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVerify_TestFail(t *testing.T) {
|
||||||
|
mainGo := `package main
|
||||||
|
|
||||||
|
func main() {}
|
||||||
|
|
||||||
|
func Broken() int {
|
||||||
|
return 42
|
||||||
|
}
|
||||||
|
`
|
||||||
|
testGo := `package main
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestBroken(t *testing.T) {
|
||||||
|
if Broken() != 99 {
|
||||||
|
t.Fatal("expected 99")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
dir := setupTempGoProject(t, mainGo, testGo)
|
||||||
|
|
||||||
|
result := Verify(dir)
|
||||||
|
|
||||||
|
if result.AllPassed {
|
||||||
|
t.Error("expected AllPassed=false")
|
||||||
|
}
|
||||||
|
if result.Build.Status != "pass" {
|
||||||
|
t.Errorf("expected build pass, got %s", result.Build.Status)
|
||||||
|
}
|
||||||
|
if result.Test.Status != "fail" {
|
||||||
|
t.Errorf("expected test fail, got %s", result.Test.Status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVerify_Duration(t *testing.T) {
|
||||||
|
mainGo := `package main
|
||||||
|
|
||||||
|
func main() {}
|
||||||
|
`
|
||||||
|
dir := setupTempGoProject(t, mainGo, "")
|
||||||
|
|
||||||
|
result := Verify(dir)
|
||||||
|
|
||||||
|
if result.Build.Duration <= 0 {
|
||||||
|
t.Error("expected build duration > 0")
|
||||||
|
}
|
||||||
|
if result.Vet.Duration <= 0 {
|
||||||
|
t.Error("expected vet duration > 0")
|
||||||
|
}
|
||||||
|
if result.Test.Duration <= 0 {
|
||||||
|
t.Error("expected test duration > 0")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVerify_OutputPopulated(t *testing.T) {
|
||||||
|
mainGo := `package main
|
||||||
|
|
||||||
|
func main() {}
|
||||||
|
`
|
||||||
|
testGo := `package main
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestNothing(t *testing.T) {}
|
||||||
|
`
|
||||||
|
dir := setupTempGoProject(t, mainGo, testGo)
|
||||||
|
|
||||||
|
result := Verify(dir)
|
||||||
|
|
||||||
|
// Test output should contain test results
|
||||||
|
if result.Test.Output == "" {
|
||||||
|
t.Error("expected non-empty test output")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseTestCount_PassingTests(t *testing.T) {
|
||||||
|
output := `=== RUN TestAdd
|
||||||
|
--- PASS: TestAdd (0.00s)
|
||||||
|
=== RUN TestSub
|
||||||
|
--- PASS: TestSub (0.00s)
|
||||||
|
PASS
|
||||||
|
ok github.com/dal/kaos 0.003s
|
||||||
|
`
|
||||||
|
count := parseTestCount(output)
|
||||||
|
if count != 2 {
|
||||||
|
t.Errorf("expected 2, got %d", count)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseTestCount_MixedResults(t *testing.T) {
|
||||||
|
output := `=== RUN TestGood
|
||||||
|
--- PASS: TestGood (0.00s)
|
||||||
|
=== RUN TestBad
|
||||||
|
--- FAIL: TestBad (0.00s)
|
||||||
|
FAIL
|
||||||
|
`
|
||||||
|
count := parseTestCount(output)
|
||||||
|
if count != 2 {
|
||||||
|
t.Errorf("expected 2, got %d", count)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseTestCount_NoTests(t *testing.T) {
|
||||||
|
output := "FAIL\n"
|
||||||
|
count := parseTestCount(output)
|
||||||
|
if count != 0 {
|
||||||
|
t.Errorf("expected 0, got %d", count)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseTestCount_Empty(t *testing.T) {
|
||||||
|
count := parseTestCount("")
|
||||||
|
if count != 0 {
|
||||||
|
t.Errorf("expected 0, got %d", count)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFormatResult(t *testing.T) {
|
||||||
|
result := VerifyResult{
|
||||||
|
Build: CheckResult{Name: "build", Status: "pass"},
|
||||||
|
Vet: CheckResult{Name: "vet", Status: "pass"},
|
||||||
|
Test: CheckResult{Name: "test", Status: "pass", TestCount: 5},
|
||||||
|
AllPassed: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
out := FormatResult(result)
|
||||||
|
if out == "" {
|
||||||
|
t.Error("expected non-empty format output")
|
||||||
|
}
|
||||||
|
if !contains(out, "All checks passed") {
|
||||||
|
t.Errorf("expected 'All checks passed' in output, got: %s", out)
|
||||||
|
}
|
||||||
|
if !contains(out, "5 tests") {
|
||||||
|
t.Errorf("expected '5 tests' in output, got: %s", out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func contains(s, substr string) bool {
|
||||||
|
return len(s) >= len(substr) && searchString(s, substr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func searchString(s, substr string) bool {
|
||||||
|
for i := 0; i <= len(s)-len(substr); i++ {
|
||||||
|
if s[i:i+len(substr)] == substr {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user