From 5d869f56cea36ed194b2ccf1b8750cf46a63c12a Mon Sep 17 00:00:00 2001 From: djuka Date: Fri, 20 Feb 2026 11:44:28 +0000 Subject: [PATCH] =?UTF-8?q?T04:=20Checker=20=E2=80=94=20verifikacija=20pos?= =?UTF-8?q?le=20agenta?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- TASKS/reports/T04-report.md | 46 ++++ TASKS/review/T04.md | 82 +++++++ code/internal/supervisor/checker.go | 113 ++++++++++ code/internal/supervisor/checker_test.go | 263 +++++++++++++++++++++++ 4 files changed, 504 insertions(+) create mode 100644 TASKS/reports/T04-report.md create mode 100644 TASKS/review/T04.md create mode 100644 code/internal/supervisor/checker.go create mode 100644 code/internal/supervisor/checker_test.go diff --git a/TASKS/reports/T04-report.md b/TASKS/reports/T04-report.md new file mode 100644 index 0000000..1b9d5e9 --- /dev/null +++ b/TASKS/reports/T04-report.md @@ -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 diff --git a/TASKS/review/T04.md b/TASKS/review/T04.md new file mode 100644 index 0000000..0cf88a1 --- /dev/null +++ b/TASKS/review/T04.md @@ -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)* diff --git a/code/internal/supervisor/checker.go b/code/internal/supervisor/checker.go new file mode 100644 index 0000000..9ba3e93 --- /dev/null +++ b/code/internal/supervisor/checker.go @@ -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() +} diff --git a/code/internal/supervisor/checker_test.go b/code/internal/supervisor/checker_test.go new file mode 100644 index 0000000..9ca51b0 --- /dev/null +++ b/code/internal/supervisor/checker_test.go @@ -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 +}