From f3cd1347abe7ae65b00a2d1bd7737fe4f39abc01 Mon Sep 17 00:00:00 2001 From: jaakko Date: Tue, 14 Apr 2026 19:20:18 +0300 Subject: [PATCH] =?UTF-8?q?CodeBench:=20Go-tuki=20=E2=80=94=20Chi=20+=20SQ?= =?UTF-8?q?Lite=20+=20httptest?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Golden example: todo-go/ (6/6 testit läpi) - todo-go.md golden reference - prompts/code-go.md koodigenerointi-prompti - Dockerfile.go-test (golang:1.23-alpine) - benchmark.mjs: LANG_CONFIG, parseTestOutput, prompt/golden-valinta Go:lle - Käyttö: node benchmark.mjs --lang go --models qwen2.5-coder:32b --- kipina-codebench/Dockerfile.go-test | 4 + kipina-codebench/benchmark.mjs | 43 +- kipina-codebench/golden-examples/todo-go.md | 466 ++++++++++++++++++ .../golden-examples/todo-go/go.mod | 23 + .../golden-examples/todo-go/go.sum | 49 ++ .../golden-examples/todo-go/handlers.go | 155 ++++++ .../golden-examples/todo-go/handlers_test.go | 171 +++++++ .../golden-examples/todo-go/main.go | 49 ++ .../golden-examples/todo-go/models.go | 29 ++ kipina-codebench/prompts/code-go.md | 69 +++ 10 files changed, 1043 insertions(+), 15 deletions(-) create mode 100644 kipina-codebench/Dockerfile.go-test create mode 100644 kipina-codebench/golden-examples/todo-go.md create mode 100644 kipina-codebench/golden-examples/todo-go/go.mod create mode 100644 kipina-codebench/golden-examples/todo-go/go.sum create mode 100644 kipina-codebench/golden-examples/todo-go/handlers.go create mode 100644 kipina-codebench/golden-examples/todo-go/handlers_test.go create mode 100644 kipina-codebench/golden-examples/todo-go/main.go create mode 100644 kipina-codebench/golden-examples/todo-go/models.go create mode 100644 kipina-codebench/prompts/code-go.md diff --git a/kipina-codebench/Dockerfile.go-test b/kipina-codebench/Dockerfile.go-test new file mode 100644 index 0000000..e0a00a5 --- /dev/null +++ b/kipina-codebench/Dockerfile.go-test @@ -0,0 +1,4 @@ +FROM golang:1.23-alpine +RUN apk add --no-cache gcc musl-dev +WORKDIR /work +ENTRYPOINT ["sh", "-c", "cp -r /src/* . && go test -v -count=1 ./... 2>&1"] diff --git a/kipina-codebench/benchmark.mjs b/kipina-codebench/benchmark.mjs index 31bdff8..1ed6c01 100644 --- a/kipina-codebench/benchmark.mjs +++ b/kipina-codebench/benchmark.mjs @@ -34,7 +34,7 @@ const OUTPUT_DIR = arg('output', `/tmp/kipina-benchmark/${TIMESTAMP}`); const RESULTS_DIR = join(__dirname, 'results'); const THINK_MODE = args.includes('--think'); const COMPACT_MODE = args.includes('--compact'); -const LANG = arg('lang', 'python'); // python | rust +const LANG = arg('lang', 'python'); // python | rust | go const ROUNDS = parseInt(arg('rounds', '1')); // 1-10 toistoa const MAX_FIX_ROUNDS = 2; @@ -53,11 +53,12 @@ const PROFILES = JSON.parse(readFileSync(join(__dirname, 'profiles.json'), 'utf- function getGoldenForModel(model) { const modelConf = PROFILES.models[model]; const goldenFile = modelConf?.golden || 'todo.md'; - // Rust-kielelle vaihda .md → -rs.md (todo.md → todo-rs.md) - if (LANG === 'rust') { - const rsFile = goldenFile.replace(/\.md$/, '-rs.md'); - const rsPath = join(GOLDEN_DIR, rsFile); - if (existsSync(rsPath)) return rsFile; + // Kielispesifi golden: todo.md → todo-rs.md / todo-go.md + const langSuffix = { rust: '-rs', go: '-go' }[LANG]; + if (langSuffix) { + const langFile = goldenFile.replace(/\.md$/, `${langSuffix}.md`); + const langPath = join(GOLDEN_DIR, langFile); + if (existsSync(langPath)) return langFile; } return goldenFile; } @@ -66,8 +67,8 @@ function getCodePromptForModel(model) { const modelConf = PROFILES.models[model]; const profile = modelConf?.profile || PROFILES.default_profile; const promptName = modelConf?.prompt || PROFILES.profiles[profile]?.prompt || 'code'; - const suffix = LANG === 'rust' ? '-rs' : ''; - // Yritä kielispesifistä ensin (code-small-rs), sitten perus (code-small) + const suffix = { rust: '-rs', go: '-go' }[LANG] || ''; + // Yritä kielispesifistä ensin (code-small-rs, code-go), sitten perus (code-small) const candidates = [`${promptName}${suffix}`, promptName, `code${suffix}`, 'code']; for (const name of candidates) { const path = join(__dirname, 'prompts', `${name}.md`); @@ -91,18 +92,24 @@ const LANG_CONFIG = { required: ['Cargo.toml', 'src/models.rs', 'src/handlers.rs', 'src/lib.rs', 'src/main.rs', 'tests/api_test.rs'], dockerImage: 'kipina-cargo-test', }, + go: { + goldenDir: 'todo-go', + files: ['go.mod', 'models.go', 'handlers.go', 'main.go', 'handlers_test.go'], + required: ['go.mod', 'models.go', 'handlers.go', 'main.go', 'handlers_test.go'], + dockerImage: 'kipina-go-test', + }, }; const LCONF = LANG_CONFIG[LANG] || LANG_CONFIG.python; function loadGoldenExample(model) { // --compact: käytä tiivistettyä templaattia if (COMPACT_MODE) { - const compactFile = LANG === 'rust' ? 'golden-compact-rs.md' : 'golden-compact-py.md'; + const compactFile = { rust: 'golden-compact-rs.md', go: 'golden-compact-go.md' }[LANG] || 'golden-compact-py.md'; const compactPath = join(__dirname, 'prompts', compactFile); if (existsSync(compactPath)) return '\n' + readFileSync(compactPath, 'utf-8').trim() + '\n'; } // Mallikohtainen golden example profiilista - const goldenFile = model ? getGoldenForModel(model) : (LANG === 'rust' ? 'todo-rs.md' : 'todo.md'); + const goldenFile = model ? getGoldenForModel(model) : ({ rust: 'todo-rs.md', go: 'todo-go.md' }[LANG] || 'todo.md'); const mdPath = join(GOLDEN_DIR, goldenFile); if (existsSync(mdPath)) return '\n' + readFileSync(mdPath, 'utf-8').trim() + '\n'; // Fallback: erilliset tiedostot @@ -201,7 +208,13 @@ function parseTestOutput(output) { const failed = parseInt(cargoMatch[2]); return { testsPassed: passed, testsFailed: failed, testsTotal: passed + failed }; } - // Cargo compilation error: count "error[E" occurrences + // Go test: "--- PASS:" / "--- FAIL:" + const goPassed = (output.match(/--- PASS:/g) || []).length; + const goFailed = (output.match(/--- FAIL:/g) || []).length; + if (goPassed + goFailed > 0) { + return { testsPassed: goPassed, testsFailed: goFailed, testsTotal: goPassed + goFailed }; + } + // Cargo/Go compilation error: count "error[E" or Go compile errors const compileErrors = (output.match(/error\[E\d+\]/g) || []).length; if (compileErrors > 0) { return { testsPassed: 0, testsFailed: compileErrors, testsTotal: compileErrors }; @@ -333,7 +346,7 @@ async function runPipeline(model, scenario) { // 3. LLM-koodigenerointi const fileCount = LCONF.required.length; const goldenExample = loadGoldenExample(model); - const codeTokens = LANG === 'rust' ? 12288 : 8192; + const codeTokens = LANG === 'rust' ? 12288 : LANG === 'go' ? 10240 : 8192; let files; // Orkestrointi: pilko entiteetti kerrallaan pienille malleille @@ -454,7 +467,7 @@ async function runPipeline(model, scenario) { result.fixRounds = fixRound; // 5. Testit Docker-kontissa + itsekorjaava looppi (Taso 4) - const testLabel = LANG === 'rust' ? 'Cargo test' : 'Pytest'; + const testLabel = { rust: 'Cargo test', go: 'Go test', python: 'Pytest' }[LANG] || 'Test'; const dockerTimeout = LANG === 'rust' ? 300000 : 120000; const MAX_TEST_FIX = 3; let bestFiles = { ...files }; // Paras versio tiedostoista @@ -538,13 +551,13 @@ async function runPipeline(model, scenario) { } // Itsekorjaus: syötä virhe + koodi mallille - const errorLines = testOut.split('\n').filter(l => /^E |FAILED|ERROR|error\[E/.test(l)).slice(0, 20).join('\n'); + const errorLines = testOut.split('\n').filter(l => /^E |FAILED|ERROR|error\[E|--- FAIL|panic:|\.go:\d+/.test(l)).slice(0, 20).join('\n'); if (!errorLines) break; // Ei parsittavia virheitä console.log(` [5/5] Itsekorjaus: ${result.testsFailed || 'virhe'}...`); const allCode = Object.entries(files).map(([fn, c]) => `=== ${fn} ===\n${c}`).join('\n\n'); const fixPrompt = `The following test errors occurred. Fix the code so ALL tests pass. Return ALL files with === markers.\n\nERRORS:\n${errorLines}\n\nCURRENT CODE:\n${allCode}`; - const fixResp = await ollamaChat(model, fixPrompt, CODE_SYSTEM, LANG === 'rust' ? 12288 : 8192); + const fixResp = await ollamaChat(model, fixPrompt, CODE_SYSTEM, LANG === 'rust' ? 12288 : LANG === 'go' ? 10240 : 8192); timings.push(fixResp); const fixedFiles = parseGeneratedFiles(fixResp.text); diff --git a/kipina-codebench/golden-examples/todo-go.md b/kipina-codebench/golden-examples/todo-go.md new file mode 100644 index 0000000..c2d3249 --- /dev/null +++ b/kipina-codebench/golden-examples/todo-go.md @@ -0,0 +1,466 @@ +# Todo — reference implementation (Go + Chi + SQLite) + +This is a complete example. Generate equivalent structure for the given project. +Use ONLY the fields from the JSON spec — do not add extras. + +## go.mod + +Chi v5 router, modernc.org/sqlite (pure Go, no CGO). + +``` +module todo-go + +go 1.23.0 + +toolchain go1.23.12 + +require ( + github.com/go-chi/chi/v5 v5.2.1 + modernc.org/sqlite v1.37.1 +) + +require ( + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/ncruces/go-strftime v0.1.9 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect + golang.org/x/sys v0.33.0 // indirect + modernc.org/libc v1.65.7 // indirect + modernc.org/mathutil v1.7.1 // indirect + modernc.org/memory v1.11.0 // indirect +) +``` + +## models.go + +Data structs: Todo (full row), CreateTodo (POST), UpdateTodo (PUT, all fields optional pointers). + +```go +package main + +// Todo represents a task with priority and status tracking. +type Todo struct { + ID int64 `json:"id"` + Title string `json:"title"` + Description *string `json:"description,omitempty"` + DueDate *string `json:"due_date,omitempty"` + Priority int64 `json:"priority"` + Status string `json:"status"` +} + +// CreateTodo is the request body for creating a new todo. +type CreateTodo struct { + Title string `json:"title"` + Description *string `json:"description,omitempty"` + DueDate *string `json:"due_date,omitempty"` + Priority *int64 `json:"priority,omitempty"` + Status *string `json:"status,omitempty"` +} + +// UpdateTodo is the request body for updating an existing todo. +type UpdateTodo struct { + Title *string `json:"title,omitempty"` + Description *string `json:"description,omitempty"` + DueDate *string `json:"due_date,omitempty"` + Priority *int64 `json:"priority,omitempty"` + Status *string `json:"status,omitempty"` +} +``` + +## handlers.go + +CRUD handlers as closures taking *sql.DB. INSERT/UPDATE use RETURNING, sql.ErrNoRows for 404. + +```go +package main + +import ( + "database/sql" + "encoding/json" + "net/http" + "strconv" + + "github.com/go-chi/chi/v5" +) + +func createTodo(db *sql.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var input CreateTodo + if err := json.NewDecoder(r.Body).Decode(&input); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + priority := int64(1) + if input.Priority != nil { + priority = *input.Priority + } + status := "pending" + if input.Status != nil { + status = *input.Status + } + var todo Todo + err := db.QueryRow( + `INSERT INTO todos (title, description, due_date, priority, status) + VALUES (?, ?, ?, ?, ?) + RETURNING id, title, description, due_date, priority, status`, + input.Title, input.Description, input.DueDate, priority, status, + ).Scan(&todo.ID, &todo.Title, &todo.Description, &todo.DueDate, &todo.Priority, &todo.Status) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + json.NewEncoder(w).Encode(todo) + } +} + +func listTodos(db *sql.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + rows, err := db.Query("SELECT id, title, description, due_date, priority, status FROM todos") + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + defer rows.Close() + var todos []Todo + for rows.Next() { + var t Todo + if err := rows.Scan(&t.ID, &t.Title, &t.Description, &t.DueDate, &t.Priority, &t.Status); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + todos = append(todos, t) + } + if todos == nil { + todos = []Todo{} + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(todos) + } +} + +func getTodo(db *sql.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + id, _ := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64) + var todo Todo + err := db.QueryRow( + "SELECT id, title, description, due_date, priority, status FROM todos WHERE id = ?", id, + ).Scan(&todo.ID, &todo.Title, &todo.Description, &todo.DueDate, &todo.Priority, &todo.Status) + if err == sql.ErrNoRows { + http.Error(w, "not found", http.StatusNotFound) + return + } + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(todo) + } +} + +func updateTodo(db *sql.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + id, _ := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64) + var existing Todo + err := db.QueryRow( + "SELECT id, title, description, due_date, priority, status FROM todos WHERE id = ?", id, + ).Scan(&existing.ID, &existing.Title, &existing.Description, &existing.DueDate, &existing.Priority, &existing.Status) + if err == sql.ErrNoRows { + http.Error(w, "not found", http.StatusNotFound) + return + } + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + var input UpdateTodo + if err := json.NewDecoder(r.Body).Decode(&input); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + if input.Title != nil { + existing.Title = *input.Title + } + if input.Description != nil { + existing.Description = input.Description + } + if input.DueDate != nil { + existing.DueDate = input.DueDate + } + if input.Priority != nil { + existing.Priority = *input.Priority + } + if input.Status != nil { + existing.Status = *input.Status + } + var updated Todo + err = db.QueryRow( + `UPDATE todos SET title = ?, description = ?, due_date = ?, priority = ?, status = ? + WHERE id = ? + RETURNING id, title, description, due_date, priority, status`, + existing.Title, existing.Description, existing.DueDate, existing.Priority, existing.Status, id, + ).Scan(&updated.ID, &updated.Title, &updated.Description, &updated.DueDate, &updated.Priority, &updated.Status) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(updated) + } +} + +func deleteTodo(db *sql.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + id, _ := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64) + result, err := db.Exec("DELETE FROM todos WHERE id = ?", id) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + rows, _ := result.RowsAffected() + if rows == 0 { + http.Error(w, "not found", http.StatusNotFound) + return + } + w.WriteHeader(http.StatusNoContent) + } +} +``` + +## main.go + +Entry point: SQLite connection, table init, Chi router on port 3000. + +```go +package main + +import ( + "database/sql" + "fmt" + "log" + "net/http" + + "github.com/go-chi/chi/v5" + _ "modernc.org/sqlite" +) + +// InitDB creates tables if they don't exist. +func InitDB(db *sql.DB) { + _, err := db.Exec(`CREATE TABLE IF NOT EXISTS todos ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + title TEXT NOT NULL, + description TEXT, + due_date TEXT, + priority INTEGER NOT NULL DEFAULT 1, + status TEXT NOT NULL DEFAULT 'pending' + )`) + if err != nil { + log.Fatal(err) + } +} + +// NewRouter creates a chi router with all routes. +func NewRouter(db *sql.DB) http.Handler { + r := chi.NewRouter() + r.Post("/todos", createTodo(db)) + r.Get("/todos", listTodos(db)) + r.Get("/todos/{id}", getTodo(db)) + r.Put("/todos/{id}", updateTodo(db)) + r.Delete("/todos/{id}", deleteTodo(db)) + return r +} + +func main() { + db, err := sql.Open("sqlite", "file:app.db?mode=rwc") + if err != nil { + log.Fatal(err) + } + defer db.Close() + InitDB(db) + + fmt.Println("Server running: http://127.0.0.1:3000") + log.Fatal(http.ListenAndServe("127.0.0.1:3000", NewRouter(db))) +} +``` + +## handlers_test.go + +Integration tests: setupTestServer with httptest.NewServer + :memory: SQLite, unique data per test. + +```go +package main + +import ( + "database/sql" + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "strings" + "testing" + + _ "modernc.org/sqlite" +) + +func setupTestServer(t *testing.T) (*httptest.Server, *sql.DB) { + t.Helper() + db, err := sql.Open("sqlite", ":memory:") + if err != nil { + t.Fatal(err) + } + InitDB(db) + return httptest.NewServer(NewRouter(db)), db +} + +func TestCreateTodo(t *testing.T) { + ts, db := setupTestServer(t) + defer ts.Close() + defer db.Close() + + resp, err := http.Post(ts.URL+"/todos", "application/json", + strings.NewReader(`{"title":"Buy groceries","priority":2}`)) + if err != nil { + t.Fatal(err) + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusCreated { + t.Fatalf("expected 201, got %d", resp.StatusCode) + } + var body map[string]interface{} + json.NewDecoder(resp.Body).Decode(&body) + if body["title"] != "Buy groceries" { + t.Fatalf("expected title 'Buy groceries', got %v", body["title"]) + } + if body["id"] == nil { + t.Fatal("expected id to be present") + } +} + +func TestListTodos(t *testing.T) { + ts, db := setupTestServer(t) + defer ts.Close() + defer db.Close() + + http.Post(ts.URL+"/todos", "application/json", + strings.NewReader(`{"title":"Listable task"}`)) + + resp, err := http.Get(ts.URL + "/todos") + if err != nil { + t.Fatal(err) + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + t.Fatalf("expected 200, got %d", resp.StatusCode) + } + var body []map[string]interface{} + json.NewDecoder(resp.Body).Decode(&body) + if len(body) < 1 { + t.Fatal("expected at least 1 todo") + } +} + +func TestGetTodoByID(t *testing.T) { + ts, db := setupTestServer(t) + defer ts.Close() + defer db.Close() + + resp, _ := http.Post(ts.URL+"/todos", "application/json", + strings.NewReader(`{"title":"Fetchable task"}`)) + var created map[string]interface{} + json.NewDecoder(resp.Body).Decode(&created) + resp.Body.Close() + + id := created["id"].(float64) + resp, err := http.Get(ts.URL + "/todos/" + fmt.Sprintf("%.0f", id)) + if err != nil { + t.Fatal(err) + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + t.Fatalf("expected 200, got %d", resp.StatusCode) + } + var body map[string]interface{} + json.NewDecoder(resp.Body).Decode(&body) + if body["id"] != id { + t.Fatalf("expected id %.0f, got %v", id, body["id"]) + } +} + +func TestGetTodoNotFound(t *testing.T) { + ts, db := setupTestServer(t) + defer ts.Close() + defer db.Close() + + resp, err := http.Get(ts.URL + "/todos/99999") + if err != nil { + t.Fatal(err) + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusNotFound { + t.Fatalf("expected 404, got %d", resp.StatusCode) + } +} + +func TestUpdateTodo(t *testing.T) { + ts, db := setupTestServer(t) + defer ts.Close() + defer db.Close() + + resp, _ := http.Post(ts.URL+"/todos", "application/json", + strings.NewReader(`{"title":"Old title"}`)) + var created map[string]interface{} + json.NewDecoder(resp.Body).Decode(&created) + resp.Body.Close() + + id := created["id"].(float64) + req, _ := http.NewRequest(http.MethodPut, ts.URL+"/todos/"+fmt.Sprintf("%.0f", id), + strings.NewReader(`{"title":"New title"}`)) + req.Header.Set("Content-Type", "application/json") + resp, err := http.DefaultClient.Do(req) + if err != nil { + t.Fatal(err) + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + t.Fatalf("expected 200, got %d", resp.StatusCode) + } + var body map[string]interface{} + json.NewDecoder(resp.Body).Decode(&body) + if body["title"] != "New title" { + t.Fatalf("expected 'New title', got %v", body["title"]) + } +} + +func TestDeleteTodo(t *testing.T) { + ts, db := setupTestServer(t) + defer ts.Close() + defer db.Close() + + resp, _ := http.Post(ts.URL+"/todos", "application/json", + strings.NewReader(`{"title":"Deletable task"}`)) + var created map[string]interface{} + json.NewDecoder(resp.Body).Decode(&created) + resp.Body.Close() + + id := created["id"].(float64) + req, _ := http.NewRequest(http.MethodDelete, ts.URL+"/todos/"+fmt.Sprintf("%.0f", id), nil) + resp, err := http.DefaultClient.Do(req) + if err != nil { + t.Fatal(err) + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusNoContent { + t.Fatalf("expected 204, got %d", resp.StatusCode) + } + + resp, _ = http.Get(ts.URL + "/todos/" + fmt.Sprintf("%.0f", id)) + defer resp.Body.Close() + if resp.StatusCode != http.StatusNotFound { + t.Fatalf("expected 404 after delete, got %d", resp.StatusCode) + } +} +``` diff --git a/kipina-codebench/golden-examples/todo-go/go.mod b/kipina-codebench/golden-examples/todo-go/go.mod new file mode 100644 index 0000000..8597e0b --- /dev/null +++ b/kipina-codebench/golden-examples/todo-go/go.mod @@ -0,0 +1,23 @@ +module todo-go + +go 1.23.0 + +toolchain go1.23.12 + +require ( + github.com/go-chi/chi/v5 v5.2.1 + modernc.org/sqlite v1.37.1 +) + +require ( + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/ncruces/go-strftime v0.1.9 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect + golang.org/x/sys v0.33.0 // indirect + modernc.org/libc v1.65.7 // indirect + modernc.org/mathutil v1.7.1 // indirect + modernc.org/memory v1.11.0 // indirect +) diff --git a/kipina-codebench/golden-examples/todo-go/go.sum b/kipina-codebench/golden-examples/todo-go/go.sum new file mode 100644 index 0000000..980d37e --- /dev/null +++ b/kipina-codebench/golden-examples/todo-go/go.sum @@ -0,0 +1,49 @@ +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8= +github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= +github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM= +golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8= +golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= +golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= +golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= +golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc= +golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI= +modernc.org/cc/v4 v4.26.1 h1:+X5NtzVBn0KgsBCBe+xkDC7twLb/jNVj9FPgiwSQO3s= +modernc.org/cc/v4 v4.26.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= +modernc.org/ccgo/v4 v4.28.0 h1:rjznn6WWehKq7dG4JtLRKxb52Ecv8OUGah8+Z/SfpNU= +modernc.org/ccgo/v4 v4.28.0/go.mod h1:JygV3+9AV6SmPhDasu4JgquwU81XAKLd3OKTUDNOiKE= +modernc.org/fileutil v1.3.1 h1:8vq5fe7jdtEvoCf3Zf9Nm0Q05sH6kGx0Op2CPx1wTC8= +modernc.org/fileutil v1.3.1/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc= +modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= +modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= +modernc.org/libc v1.65.7 h1:Ia9Z4yzZtWNtUIuiPuQ7Qf7kxYrxP1/jeHZzG8bFu00= +modernc.org/libc v1.65.7/go.mod h1:011EQibzzio/VX3ygj1qGFt5kMjP0lHb0qCW5/D/pQU= +modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= +modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= +modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= +modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= +modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= +modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= +modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= +modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= +modernc.org/sqlite v1.37.1 h1:EgHJK/FPoqC+q2YBXg7fUmES37pCHFc97sI7zSayBEs= +modernc.org/sqlite v1.37.1/go.mod h1:XwdRtsE1MpiBcL54+MbKcaDvcuej+IYSMfLN6gSKV8g= +modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= +modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= diff --git a/kipina-codebench/golden-examples/todo-go/handlers.go b/kipina-codebench/golden-examples/todo-go/handlers.go new file mode 100644 index 0000000..d600de5 --- /dev/null +++ b/kipina-codebench/golden-examples/todo-go/handlers.go @@ -0,0 +1,155 @@ +package main + +import ( + "database/sql" + "encoding/json" + "net/http" + "strconv" + + "github.com/go-chi/chi/v5" +) + +func createTodo(db *sql.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var input CreateTodo + if err := json.NewDecoder(r.Body).Decode(&input); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + priority := int64(1) + if input.Priority != nil { + priority = *input.Priority + } + status := "pending" + if input.Status != nil { + status = *input.Status + } + var todo Todo + err := db.QueryRow( + `INSERT INTO todos (title, description, due_date, priority, status) + VALUES (?, ?, ?, ?, ?) + RETURNING id, title, description, due_date, priority, status`, + input.Title, input.Description, input.DueDate, priority, status, + ).Scan(&todo.ID, &todo.Title, &todo.Description, &todo.DueDate, &todo.Priority, &todo.Status) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + json.NewEncoder(w).Encode(todo) + } +} + +func listTodos(db *sql.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + rows, err := db.Query("SELECT id, title, description, due_date, priority, status FROM todos") + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + defer rows.Close() + var todos []Todo + for rows.Next() { + var t Todo + if err := rows.Scan(&t.ID, &t.Title, &t.Description, &t.DueDate, &t.Priority, &t.Status); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + todos = append(todos, t) + } + if todos == nil { + todos = []Todo{} + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(todos) + } +} + +func getTodo(db *sql.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + id, _ := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64) + var todo Todo + err := db.QueryRow( + "SELECT id, title, description, due_date, priority, status FROM todos WHERE id = ?", id, + ).Scan(&todo.ID, &todo.Title, &todo.Description, &todo.DueDate, &todo.Priority, &todo.Status) + if err == sql.ErrNoRows { + http.Error(w, "not found", http.StatusNotFound) + return + } + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(todo) + } +} + +func updateTodo(db *sql.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + id, _ := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64) + var existing Todo + err := db.QueryRow( + "SELECT id, title, description, due_date, priority, status FROM todos WHERE id = ?", id, + ).Scan(&existing.ID, &existing.Title, &existing.Description, &existing.DueDate, &existing.Priority, &existing.Status) + if err == sql.ErrNoRows { + http.Error(w, "not found", http.StatusNotFound) + return + } + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + var input UpdateTodo + if err := json.NewDecoder(r.Body).Decode(&input); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + if input.Title != nil { + existing.Title = *input.Title + } + if input.Description != nil { + existing.Description = input.Description + } + if input.DueDate != nil { + existing.DueDate = input.DueDate + } + if input.Priority != nil { + existing.Priority = *input.Priority + } + if input.Status != nil { + existing.Status = *input.Status + } + var updated Todo + err = db.QueryRow( + `UPDATE todos SET title = ?, description = ?, due_date = ?, priority = ?, status = ? + WHERE id = ? + RETURNING id, title, description, due_date, priority, status`, + existing.Title, existing.Description, existing.DueDate, existing.Priority, existing.Status, id, + ).Scan(&updated.ID, &updated.Title, &updated.Description, &updated.DueDate, &updated.Priority, &updated.Status) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(updated) + } +} + +func deleteTodo(db *sql.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + id, _ := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64) + result, err := db.Exec("DELETE FROM todos WHERE id = ?", id) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + rows, _ := result.RowsAffected() + if rows == 0 { + http.Error(w, "not found", http.StatusNotFound) + return + } + w.WriteHeader(http.StatusNoContent) + } +} diff --git a/kipina-codebench/golden-examples/todo-go/handlers_test.go b/kipina-codebench/golden-examples/todo-go/handlers_test.go new file mode 100644 index 0000000..da59d6f --- /dev/null +++ b/kipina-codebench/golden-examples/todo-go/handlers_test.go @@ -0,0 +1,171 @@ +package main + +import ( + "database/sql" + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "strings" + "testing" + + _ "modernc.org/sqlite" +) + +func setupTestServer(t *testing.T) (*httptest.Server, *sql.DB) { + t.Helper() + db, err := sql.Open("sqlite", ":memory:") + if err != nil { + t.Fatal(err) + } + InitDB(db) + return httptest.NewServer(NewRouter(db)), db +} + +func TestCreateTodo(t *testing.T) { + ts, db := setupTestServer(t) + defer ts.Close() + defer db.Close() + + resp, err := http.Post(ts.URL+"/todos", "application/json", + strings.NewReader(`{"title":"Buy groceries","priority":2}`)) + if err != nil { + t.Fatal(err) + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusCreated { + t.Fatalf("expected 201, got %d", resp.StatusCode) + } + var body map[string]interface{} + json.NewDecoder(resp.Body).Decode(&body) + if body["title"] != "Buy groceries" { + t.Fatalf("expected title 'Buy groceries', got %v", body["title"]) + } + if body["id"] == nil { + t.Fatal("expected id to be present") + } +} + +func TestListTodos(t *testing.T) { + ts, db := setupTestServer(t) + defer ts.Close() + defer db.Close() + + http.Post(ts.URL+"/todos", "application/json", + strings.NewReader(`{"title":"Listable task"}`)) + + resp, err := http.Get(ts.URL + "/todos") + if err != nil { + t.Fatal(err) + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + t.Fatalf("expected 200, got %d", resp.StatusCode) + } + var body []map[string]interface{} + json.NewDecoder(resp.Body).Decode(&body) + if len(body) < 1 { + t.Fatal("expected at least 1 todo") + } +} + +func TestGetTodoByID(t *testing.T) { + ts, db := setupTestServer(t) + defer ts.Close() + defer db.Close() + + resp, _ := http.Post(ts.URL+"/todos", "application/json", + strings.NewReader(`{"title":"Fetchable task"}`)) + var created map[string]interface{} + json.NewDecoder(resp.Body).Decode(&created) + resp.Body.Close() + + id := created["id"].(float64) + resp, err := http.Get(ts.URL + "/todos/" + fmt.Sprintf("%.0f", id)) + if err != nil { + t.Fatal(err) + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + t.Fatalf("expected 200, got %d", resp.StatusCode) + } + var body map[string]interface{} + json.NewDecoder(resp.Body).Decode(&body) + if body["id"] != id { + t.Fatalf("expected id %.0f, got %v", id, body["id"]) + } +} + +func TestGetTodoNotFound(t *testing.T) { + ts, db := setupTestServer(t) + defer ts.Close() + defer db.Close() + + resp, err := http.Get(ts.URL + "/todos/99999") + if err != nil { + t.Fatal(err) + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusNotFound { + t.Fatalf("expected 404, got %d", resp.StatusCode) + } +} + +func TestUpdateTodo(t *testing.T) { + ts, db := setupTestServer(t) + defer ts.Close() + defer db.Close() + + resp, _ := http.Post(ts.URL+"/todos", "application/json", + strings.NewReader(`{"title":"Old title"}`)) + var created map[string]interface{} + json.NewDecoder(resp.Body).Decode(&created) + resp.Body.Close() + + id := created["id"].(float64) + req, _ := http.NewRequest(http.MethodPut, ts.URL+"/todos/"+fmt.Sprintf("%.0f", id), + strings.NewReader(`{"title":"New title"}`)) + req.Header.Set("Content-Type", "application/json") + resp, err := http.DefaultClient.Do(req) + if err != nil { + t.Fatal(err) + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + t.Fatalf("expected 200, got %d", resp.StatusCode) + } + var body map[string]interface{} + json.NewDecoder(resp.Body).Decode(&body) + if body["title"] != "New title" { + t.Fatalf("expected 'New title', got %v", body["title"]) + } +} + +func TestDeleteTodo(t *testing.T) { + ts, db := setupTestServer(t) + defer ts.Close() + defer db.Close() + + resp, _ := http.Post(ts.URL+"/todos", "application/json", + strings.NewReader(`{"title":"Deletable task"}`)) + var created map[string]interface{} + json.NewDecoder(resp.Body).Decode(&created) + resp.Body.Close() + + id := created["id"].(float64) + req, _ := http.NewRequest(http.MethodDelete, ts.URL+"/todos/"+fmt.Sprintf("%.0f", id), nil) + resp, err := http.DefaultClient.Do(req) + if err != nil { + t.Fatal(err) + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusNoContent { + t.Fatalf("expected 204, got %d", resp.StatusCode) + } + + resp, _ = http.Get(ts.URL + "/todos/" + fmt.Sprintf("%.0f", id)) + defer resp.Body.Close() + if resp.StatusCode != http.StatusNotFound { + t.Fatalf("expected 404 after delete, got %d", resp.StatusCode) + } +} diff --git a/kipina-codebench/golden-examples/todo-go/main.go b/kipina-codebench/golden-examples/todo-go/main.go new file mode 100644 index 0000000..8e5cb6b --- /dev/null +++ b/kipina-codebench/golden-examples/todo-go/main.go @@ -0,0 +1,49 @@ +package main + +import ( + "database/sql" + "fmt" + "log" + "net/http" + + "github.com/go-chi/chi/v5" + _ "modernc.org/sqlite" +) + +// InitDB creates tables if they don't exist. +func InitDB(db *sql.DB) { + _, err := db.Exec(`CREATE TABLE IF NOT EXISTS todos ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + title TEXT NOT NULL, + description TEXT, + due_date TEXT, + priority INTEGER NOT NULL DEFAULT 1, + status TEXT NOT NULL DEFAULT 'pending' + )`) + if err != nil { + log.Fatal(err) + } +} + +// NewRouter creates a chi router with all routes. +func NewRouter(db *sql.DB) http.Handler { + r := chi.NewRouter() + r.Post("/todos", createTodo(db)) + r.Get("/todos", listTodos(db)) + r.Get("/todos/{id}", getTodo(db)) + r.Put("/todos/{id}", updateTodo(db)) + r.Delete("/todos/{id}", deleteTodo(db)) + return r +} + +func main() { + db, err := sql.Open("sqlite", "file:app.db?mode=rwc") + if err != nil { + log.Fatal(err) + } + defer db.Close() + InitDB(db) + + fmt.Println("Server running: http://127.0.0.1:3000") + log.Fatal(http.ListenAndServe("127.0.0.1:3000", NewRouter(db))) +} diff --git a/kipina-codebench/golden-examples/todo-go/models.go b/kipina-codebench/golden-examples/todo-go/models.go new file mode 100644 index 0000000..28db05e --- /dev/null +++ b/kipina-codebench/golden-examples/todo-go/models.go @@ -0,0 +1,29 @@ +package main + +// Todo represents a task with priority and status tracking. +type Todo struct { + ID int64 `json:"id"` + Title string `json:"title"` + Description *string `json:"description,omitempty"` + DueDate *string `json:"due_date,omitempty"` + Priority int64 `json:"priority"` + Status string `json:"status"` +} + +// CreateTodo is the request body for creating a new todo. +type CreateTodo struct { + Title string `json:"title"` + Description *string `json:"description,omitempty"` + DueDate *string `json:"due_date,omitempty"` + Priority *int64 `json:"priority,omitempty"` + Status *string `json:"status,omitempty"` +} + +// UpdateTodo is the request body for updating an existing todo. +type UpdateTodo struct { + Title *string `json:"title,omitempty"` + Description *string `json:"description,omitempty"` + DueDate *string `json:"due_date,omitempty"` + Priority *int64 `json:"priority,omitempty"` + Status *string `json:"status,omitempty"` +} diff --git a/kipina-codebench/prompts/code-go.md b/kipina-codebench/prompts/code-go.md new file mode 100644 index 0000000..4dcccc0 --- /dev/null +++ b/kipina-codebench/prompts/code-go.md @@ -0,0 +1,69 @@ +You are a Go backend developer. Generate a Chi web project with SQLite. + +Given the project requirements, JSON specification, and a REFERENCE IMPLEMENTATION, generate these files: + +1. go.mod — module declaration, go-chi/chi/v5, modernc.org/sqlite +2. models.go — Structs with json tags +3. handlers.go — Handler closures for each CRUD endpoint +4. main.go — Entry point with InitDB(), NewRouter(), main() +5. handlers_test.go — Integration tests using httptest against in-memory SQLite + +Do NOT generate any other files. Do NOT generate go.sum. + +OUTPUT FORMAT — use these exact markers to separate files: + +=== go.mod === + + +=== models.go === + + +=== handlers.go === + + +=== main.go === + + +=== handlers_test.go === + + +DOCUMENTATION — structs get // one-line comments. Keep it brief. + +RULES: +- Follow the REFERENCE IMPLEMENTATION patterns exactly +- Chi router with chi.URLParam(r, "param") for path parameters +- database/sql + modernc.org/sqlite (pure Go driver, no CGO required) +- Import the driver as blank import: _ "modernc.org/sqlite" +- Handlers are closures: func handler(db *sql.DB) http.HandlerFunc +- INSERT/UPDATE queries use RETURNING clause to get the row back via QueryRow + Scan +- POST returns 201 (http.StatusCreated), DELETE returns 204 (http.StatusNoContent), GET missing returns 404 +- Use sql.ErrNoRows for not-found checks: if err == sql.ErrNoRows { ... } +- No compile-time query macros — use db.QueryRow(), db.Query(), db.Exec() directly +- Empty slice not nil for list endpoints: if items == nil { items = []Item{} } +- Optional fields use pointer types (*string, *int64) with json tag omitempty +- Set Content-Type header: w.Header().Set("Content-Type", "application/json") +- Parse path ID with strconv.ParseInt(chi.URLParam(r, "id"), 10, 64) +- InitDB uses log.Fatal on error, NewRouter returns http.Handler +- main() opens "file:app.db?mode=rwc" and listens on 127.0.0.1:3000 +- No markdown fences inside file content — just raw code +- You MUST generate ALL 5 files. Do not stop early. + +TESTS — follow this exact setupTestServer pattern: + +func setupTestServer(t *testing.T) (*httptest.Server, *sql.DB) { + t.Helper() + db, err := sql.Open("sqlite", ":memory:") + if err != nil { + t.Fatal(err) + } + InitDB(db) + return httptest.NewServer(NewRouter(db)), db +} + +- Each test function calls setupTestServer(t) to get (ts, db) +- defer ts.Close() and defer db.Close() in every test +- Use standard library: http.Post, http.Get, http.NewRequest for PUT/DELETE +- Use strings.NewReader for JSON request bodies +- Decode responses with json.NewDecoder(resp.Body).Decode(&body) +- Unique descriptive data, NOT generic "test" strings +- Format IDs with fmt.Sprintf("%.0f", id) when building URLs from float64