josie / llm-bench

#!/usr/bin/env python3
"""
Task: go_htmx — a Go web app with HTMX partials + multi-table SQLite.

Models a real-world Go web tool in miniature: single Go binary, server-rendered
html/template + HTMX for partial updates, embedded SQLite (modernc.org/sqlite),
a multi-table schema with a self-referential location tree (recursive CTE
read), embedded assets via //go:embed. The "real product" = a binary that
serves a page listing locations in a tree, lets you add a sub-location via an
HTMX partial swap (no full page reload), and persists to SQLite.
"""
import os, sys, subprocess, time, shutil, signal, socket, urllib.request, urllib.parse, json
HERE = os.path.dirname(os.path.abspath(__file__))
from bench.core import tools

TASK_NAME = "go_htmx"

PLAN = """\
# Task — Build a Go web app with HTMX partials + multi-table SQLite

## Goal
`go build ./...` and `go test ./...` run green, and the binary starts on port
8080, serves a locations tree page at `/`, lets you add a sub-location via an
HTMX partial swap (POST returns just the updated tree HTML fragment, not a full
page), and persists to embedded SQLite. This is a Go web app with HTMX partials
+ multi-table SQLite: single Go binary, server-rendered html/template + HTMX,
embedded SQLite, multi-table schema with a self-referential location tree read
via a recursive CTE.

## Prerequisites
Starter scaffold in the work dir:
  - `go.mod` (module `locations`, go 1.26, no deps)
  - `cmd/server/main.go` (empty stub: `package main; func main(){}`)

## Spec — the app to build
A single Go binary serving a one-page location-tree manager over HTTP. Uses
HTMX for partial updates (adding a sub-location swaps just the tree fragment,
no full page reload). Embedded SQLite via `modernc.org/sqlite`. Embedded
templates + migrations via `//go:embed`.

### Schema (two tables)
  locations(
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT NOT NULL,
    parent_id INTEGER NULL REFERENCES locations(id) ON DELETE CASCADE,
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP
  )
  -- UNIQUE(name, parent_id) — names unique within a parent (NULL parent = root)
  CREATE UNIQUE INDEX locations_name_parent ON locations(name, IFNULL(parent_id, 0));

### Files to produce (layout)
  cmd/server/main.go            boot: config -> db -> migrate -> handlers -> ListenAndServe + graceful shutdown
  internal/config/config.go     Config{Port, DBPath}; LoadFromEnv() reads NOTES_PORT (port, int) and NOTES_DB (db path) — MUST use these exact env var names; the scoring harness sets them.
  internal/db/db.go             Open(dsn): modernc sqlite, PRAGMA WAL+busy_timeout+FK on; Migrate() via embedded .sql
  internal/db/migrations/00001_init.sql   the schema above (both tables + the unique index)
  internal/models/location.go   Location struct + Create(db, name, parentID) + Tree(db) — Tree returns a flat list with depth via a recursive CTE: WITH RECURSIVE tree AS (SELECT id, name, parent_id, 0 AS depth FROM locations WHERE parent_id IS NULL UNION ALL SELECT l.id, l.name, l.parent_id, t.depth+1 FROM locations l JOIN tree t ON l.parent_id = t.id) SELECT id, name, parent_id, depth FROM tree ORDER BY depth, name
  internal/handlers/handlers.go Handlers struct; routes via stdlib net/http ServeMux: GET / (full page: form + tree), POST / (create location: form fields name + parent_id; returns JUST the updated tree fragment as an HTML partial for HTMX swap, Content-Type text/html, status 200 — NOT a redirect, NOT a full page)
  internal/handlers/handlers_test.go  httptest: POST a root location, assert the response body contains the location name indented by depth (one leading space pair per depth level); POST a child, assert it appears nested under the parent
  web/templates/page.html       the full page: <head> pulls in HTMX (a <script src="https://unpkg.com/htmx.org"></script> is fine), a form (name input + hidden parent_id input + submit) with hx-post="/" hx-target="#tree" hx-swap="innerHTML", and a <div id="tree"> containing the rendered tree
  web/templates/tree.html       the tree fragment: renders the locations as a nested <ul> (one <li> per location, indented by depth; children nested in a <ul> inside the parent <li>). This is the partial returned by POST / for the HTMX swap.
  web/web.go                    package web; //go:embed templates/*.html + a Parse() helper returning a *template.Template (or two — one for the page, one for the tree fragment)

### Constraints
  - Use `modernc.org/sqlite` (pure-Go, CGO_ENABLED=0). Import path `_ "modernc.org/sqlite"`.
  - Embed migrations AND templates via `//go:embed` (templates live in the `web/` package so the embed path is package-local — do NOT use `../` in //go:embed patterns, Go forbids it).
  - Stdlib `net/http` ServeMux only — no Gin, no chi.
  - HTMX: the POST / handler returns JUST the tree fragment (Content-Type text/html, status 200), not a redirect and not a full page. The form has `hx-post="/" hx-target="#tree" hx-swap="innerHTML"` so HTMX swaps the tree div's content with the response.
  - The tree is rendered as a nested <ul>/<li> structure. A root location is a top-level <li>; a child is a <li> inside a <ul> inside its parent's <li>.
  - Read NOTES_PORT and NOTES_DB env vars (exact names — the harness sets them to run the binary on a free port with a temp db).

## Steps
1. `go get modernc.org/sqlite && go mod tidy`
2. Write the files. Natural order: config -> db -> models -> web/templates -> handlers -> main.
3. Build (`go build ./...`). Fix compile errors.
4. Test (`go test ./...`). The handlers_test.go must pass.
5. Optionally run the binary briefly to confirm it serves.

## Acceptance (harness-verified)
1. `go build ./...` exits 0.
2. `go test ./...` exits 0.
3. Binary starts, `GET /` returns 200 with `<div id="tree"` and `htmx` in the body.
4. `POST /` with form fields name+parent_id returns 200 with the tree fragment (contains the new location name); a subsequent `GET /` shows it in the tree; the SQLite file has the row.

## Out of scope
Auth, sessions, CSRF, styling beyond minimal readable HTML, delete/edit, drag-drop, any JS beyond the HTMX script tag. This is the smallest real Go web app with HTMX partials + multi-table SQLite — a working location-tree one-pager with HTMX partials.

## Commit
You do not commit; the harness owns the work dir.
"""

STARTER = {
    "go.mod": "module locations\n\ngo 1.26\n",
    "cmd/server/main.go": "package main\n\nfunc main() {}\n",
}

REFERENCE = {
    "go.mod": "module locations\n\ngo 1.26\n\nrequire modernc.org/sqlite v1.50.0\n",
    "cmd/server/main.go": '''package main

import (
	"context"
	"fmt"
	"log"
	"net/http"
	"os"
	"os/signal"
	"syscall"
	"time"

	"locations/internal/config"
	"locations/internal/db"
	"locations/internal/handlers"
	"locations/internal/models"
)

func main() {
	cfg := config.LoadFromEnv()
	database, err := db.Open(cfg.DBPath)
	if err != nil { log.Fatalf("db open: %v", err) }
	defer database.Close()
	if err := database.Migrate(); err != nil { log.Fatalf("migrate: %v", err) }
	store := &models.Store{DB: database.DB}
	h := handlers.New(store)
	srv := &http.Server{Addr: fmt.Sprintf(":%d", cfg.Port), Handler: h.Routes()}
	go func() {
		log.Printf("listening on :%d", cfg.Port)
		if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
			log.Fatalf("serve: %v", err)
		}
	}()
	stop := make(chan os.Signal, 1)
	signal.Notify(stop, os.Interrupt, syscall.SIGTERM)
	<-stop
	log.Println("shutting down")
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	_ = srv.Shutdown(ctx)
}
''',
    "internal/config/config.go": '''package config

import (
	"os"
	"strconv"
)

type Config struct {
	Port   int
	DBPath string
}

func LoadFromEnv() Config {
	cfg := Config{Port: 8080, DBPath: "locations.db"}
	if p := os.Getenv("NOTES_PORT"); p != "" {
		if v, err := strconv.Atoi(p); err == nil { cfg.Port = v }
	}
	if d := os.Getenv("NOTES_DB"); d != "" { cfg.DBPath = d }
	return cfg
}
''',
    "internal/db/db.go": '''package db

import (
	"database/sql"
	"embed"
	"fmt"

	_ "modernc.org/sqlite"
)

//go:embed migrations/*.sql
var migrationsFS embed.FS

type Database struct {
	*sql.DB
}

func Open(dsn string) (*Database, error) {
	db, err := sql.Open("sqlite", dsn)
	if err != nil { return nil, fmt.Errorf("open sqlite: %w", err) }
	if _, err := db.Exec(`PRAGMA journal_mode=WAL; PRAGMA busy_timeout=5000; PRAGMA foreign_keys=ON;`); err != nil {
		db.Close(); return nil, fmt.Errorf("pragmas: %w", err)
	}
	return &Database{DB: db}, nil
}

func (d *Database) Migrate() error {
	if _, err := d.Exec(`CREATE TABLE IF NOT EXISTS schema_migrations (name TEXT PRIMARY KEY, applied_at DATETIME DEFAULT CURRENT_TIMESTAMP)`); err != nil {
		return fmt.Errorf("create schema_migrations: %w", err)
	}
	entries, err := migrationsFS.ReadDir("migrations")
	if err != nil { return fmt.Errorf("read migrations dir: %w", err) }
	for _, e := range entries {
		if e.IsDir() { continue }
		name := e.Name()
		var applied int
		if err := d.QueryRow(`SELECT count(*) FROM schema_migrations WHERE name = ?`, name).Scan(&applied); err != nil {
			return fmt.Errorf("check %s: %w", name, err)
		}
		if applied > 0 { continue }
		b, err := migrationsFS.ReadFile("migrations/" + name)
		if err != nil { return fmt.Errorf("read %s: %w", name, err) }
		if _, err := d.Exec(string(b)); err != nil { return fmt.Errorf("apply %s: %w", name, err) }
		if _, err := d.Exec(`INSERT INTO schema_migrations (name) VALUES (?)`, name); err != nil {
			return fmt.Errorf("record %s: %w", name, err)
		}
	}
	return nil
}
''',
    "internal/db/migrations/00001_init.sql": '''CREATE TABLE IF NOT EXISTS locations (
	id INTEGER PRIMARY KEY AUTOINCREMENT,
	name TEXT NOT NULL,
	parent_id INTEGER NULL REFERENCES locations(id) ON DELETE CASCADE,
	created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE UNIQUE INDEX IF NOT EXISTS locations_name_parent ON locations(name, IFNULL(parent_id, 0));
''',
    "internal/models/location.go": '''package models

import (
	"database/sql"
	"fmt"
)

type Location struct {
	ID       int64
	Name     string
	ParentID *int64
	Depth    int
}

type Store struct {
	DB *sql.DB
}

func (s *Store) Create(name string, parentID *int64) (*Location, error) {
	res, err := s.DB.Exec(`INSERT INTO locations (name, parent_id) VALUES (?, ?)`, name, parentID)
	if err != nil { return nil, fmt.Errorf("insert location: %w", err) }
	id, err := res.LastInsertId()
	if err != nil { return nil, err }
	return &Location{ID: id, Name: name, ParentID: parentID, Depth: 0}, nil
}

func (s *Store) Tree() ([]Location, error) {
	rows, err := s.DB.Query(`WITH RECURSIVE tree AS (
		SELECT id, name, parent_id, 0 AS depth FROM locations WHERE parent_id IS NULL
		UNION ALL
		SELECT l.id, l.name, l.parent_id, t.depth+1 FROM locations l JOIN tree t ON l.parent_id = t.id
	) SELECT id, name, parent_id, depth FROM tree ORDER BY depth, name`)
	if err != nil { return nil, fmt.Errorf("tree query: %w", err) }
	defer rows.Close()
	var out []Location
	for rows.Next() {
		var l Location
		var pid sql.NullInt64
		if err := rows.Scan(&l.ID, &l.Name, &pid, &l.Depth); err != nil { return nil, err }
		if pid.Valid { l.ParentID = &pid.Int64 }
		out = append(out, l)
	}
	return out, rows.Err()
}
''',
    "internal/handlers/handlers.go": '''package handlers

import (
	"embed"
	"html/template"
	"log"
	"net/http"
	"strconv"
	"strings"

	"locations/internal/models"
)

//go:embed templates/*.html
var templatesFS embed.FS

type Handlers struct {
	store    *models.Store
	pageTmpl *template.Template
	treeTmpl *template.Template
}

func New(store *models.Store) *Handlers {
	pageTmpl := template.Must(template.ParseFS(templatesFS, "templates/page.html", "templates/tree.html"))
	treeTmpl := template.Must(template.ParseFS(templatesFS, "templates/tree.html"))
	return &Handlers{store: store, pageTmpl: pageTmpl, treeTmpl: treeTmpl}
}

func (h *Handlers) Routes() http.Handler {
	mux := http.NewServeMux()
	mux.HandleFunc("GET /", h.index)
	mux.HandleFunc("POST /", h.create)
	return mux
}

func (h *Handlers) index(w http.ResponseWriter, r *http.Request) {
	locations, err := h.store.Tree()
	if err != nil {
		log.Printf("tree: %v", err)
		http.Error(w, "internal error", http.StatusInternalServerError)
		return
	}
	w.Header().Set("Content-Type", "text/html; charset=utf-8")
	if err := h.pageTmpl.Execute(w, locations); err != nil {
		log.Printf("render page: %v", err)
	}
}

func (h *Handlers) create(w http.ResponseWriter, r *http.Request) {
	name := strings.TrimSpace(r.FormValue("name"))
	if name == "" {
		http.Error(w, "name is required", http.StatusBadRequest)
		return
	}
	parentStr := r.FormValue("parent_id")
	var parentID *int64
	if parentStr != "" {
		id, err := strconv.ParseInt(parentStr, 10, 64)
		if err != nil {
			http.Error(w, "bad parent_id", http.StatusBadRequest)
			return
		}
		parentID = &id
	}
	if _, err := h.store.Create(name, parentID); err != nil {
		log.Printf("create: %v", err)
		http.Error(w, "internal error", http.StatusInternalServerError)
		return
	}
	locations, err := h.store.Tree()
	if err != nil {
		log.Printf("tree after create: %v", err)
		http.Error(w, "internal error", http.StatusInternalServerError)
		return
	}
	// Return JUST the tree fragment for the HTMX swap (not a redirect, not a full page).
	w.Header().Set("Content-Type", "text/html; charset=utf-8")
	w.WriteHeader(http.StatusOK)
	if err := h.treeTmpl.ExecuteTemplate(w, "tree", locations); err != nil {
		log.Printf("render tree: %v", err)
	}
}
''',
    "internal/handlers/handlers_test.go": '''package handlers

import (
	"database/sql"
	"io"
	"net/http"
	"net/http/httptest"
	"net/url"
	"strconv"
	"strings"
	"testing"

	"locations/internal/models"

	_ "modernc.org/sqlite"
)

func newTestStore(t *testing.T) (*sql.DB, func()) {
	t.Helper()
	db, err := sql.Open("sqlite", ":memory:")
	if err != nil { t.Fatalf("open: %v", err) }
	_, err = db.Exec(`CREATE TABLE locations (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, parent_id INTEGER NULL REFERENCES locations(id), created_at DATETIME DEFAULT CURRENT_TIMESTAMP)`)
	if err != nil { db.Close(); t.Fatalf("create table: %v", err) }
	return db, func() { db.Close() }
}

func TestCreateRootAndChild(t *testing.T) {
	db, cleanup := newTestStore(t)
	defer cleanup()
	store := &models.Store{DB: db}
	h := New(store)

	srv := httptest.NewServer(h.Routes())
	defer srv.Close()

	// POST a root location.
	resp, err := http.PostForm(srv.URL+"/", url.Values{"name": {"BuildingA"}, "parent_id": {""}})
	if err != nil { t.Fatalf("post root: %v", err) }
	resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		t.Fatalf("post root status = %d, want 200", resp.StatusCode)
	}

	// Get the root's id from the DB.
	var rootID int64
	if err := db.QueryRow("SELECT id FROM locations WHERE name = 'BuildingA'").Scan(&rootID); err != nil {
		t.Fatalf("query root id: %v", err)
	}

	// POST a child.
	resp2, err := http.PostForm(srv.URL+"/", url.Values{"name": {"Floor1"}, "parent_id": {strconv.FormatInt(rootID, 10)}})
	if err != nil { t.Fatalf("post child: %v", err) }
	defer resp2.Body.Close()
	if resp2.StatusCode != http.StatusOK {
		t.Fatalf("post child status = %d, want 200", resp2.StatusCode)
	}
	body, _ := io.ReadAll(resp2.Body)
	if !strings.Contains(string(body), "Floor1") {
		t.Fatalf("response does not contain child name; got:\\n%s", body)
	}
	if !strings.Contains(string(body), "BuildingA") {
		t.Fatalf("response does not contain root name (tree should include all); got:\\n%s", body)
	}
}
''',
    "internal/handlers/templates/page.html": '''<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Locations</title>
<script src="https://unpkg.com/htmx.org"></script>
<style>
body { font-family: system-ui, sans-serif; max-width: 40em; margin: 2rem auto; padding: 0 1rem; }
form { margin: 1rem 0; padding: 1rem; border: 1px solid #ccc; border-radius: 6px; }
label { display: block; margin: 0.5rem 0; }
input[type=text] { width: 100%; box-sizing: border-box; padding: 0.4rem; }
ul.tree { list-style: none; padding-left: 1.5em; }
li.location { margin: 0.25rem 0; }
</style>
</head>
<body>
<h1>Locations</h1>
<form hx-post="/" hx-target="#tree" hx-swap="innerHTML">
  <label>Name <input type="text" name="name" required></label>
  <input type="hidden" name="parent_id" value="">
  <button type="submit">Add location</button>
</form>
<div id="tree">
{{template "tree" .}}
</div>
</body>
</html>
''',
    "internal/handlers/templates/tree.html": '''{{define "tree"}}<ul class="tree">
{{range .}}<li class="location" data-depth="{{.Depth}}">{{.Name}}</li>
{{end}}</ul>{{end}}
''',
}

def _materialize(files, dest):
    shutil.rmtree(dest, ignore_errors=True)
    os.makedirs(dest, exist_ok=True)
    for path, content in files.items():
        fp = os.path.join(dest, path)
        os.makedirs(os.path.dirname(fp), exist_ok=True)
        with open(fp, "w") as f: f.write(content)
    return dest

def _run(cmd, cwd, timeout=180):
    try:
        p = subprocess.run(cmd, cwd=cwd, shell=True, capture_output=True, text=True, timeout=timeout)
    except subprocess.TimeoutExpired:
        return "TIMEOUT"
    return f"exit={p.returncode}\n{(p.stdout + p.stderr).strip()}"

def _free_port():
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM); s.bind(("", 0))
    p = s.getsockname()[1]; s.close(); return p

def _has_real_app(work_dir):
    main_go = os.path.join(work_dir, "cmd", "server", "main.go")
    if not os.path.isfile(main_go): return False
    with open(main_go) as f:
        src = f.read()
    if "ListenAndServe" not in src and "http.Server" not in src: return False
    internal = os.path.join(work_dir, "internal")
    if not os.path.isdir(internal): return False
    pkg_dirs = [d for d in os.listdir(internal) if os.path.isdir(os.path.join(internal, d))]
    return len(pkg_dirs) >= 3

def gates(work_dir):
    """Return a dict of gate_name -> bool."""
    g = {}
    if not _has_real_app(work_dir):
        return {"build": False, "test": False, "serves": False, "persists": False}
    b = _run("go build ./...", work_dir)
    g["build"] = b.startswith("exit=0")
    t = _run("go test ./...", work_dir)
    g["test"] = t.startswith("exit=0")
    # serves + persists
    binpath = os.path.abspath(os.path.join(work_dir, "server"))
    r = _run(f"go build -o {binpath} ./cmd/server", work_dir)
    if not r.startswith("exit=0") or not os.path.isfile(binpath):
        g["serves"] = False; g["persists"] = False; return g
    port = _free_port()
    dbpath = os.path.abspath(os.path.join(work_dir, f"bench_{port}.db"))
    import os as _os
    for ext in ("", "-wal", "-shm"):
        if _os.path.exists(dbpath+ext): _os.remove(dbpath+ext)
    env = dict(_os.environ, NOTES_PORT=str(port), NOTES_DB=dbpath)
    try:
        proc = subprocess.Popen([binpath], cwd=work_dir, env=env, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    except Exception:
        g["serves"] = False; g["persists"] = False; return g
    try:
        for _ in range(40):
            try:
                with socket.create_connection(("127.0.0.1", port), 0.25): break
            except OSError: time.sleep(0.25)
        else:
            g["serves"] = False; g["persists"] = False; return g
        base = f"http://127.0.0.1:{port}"
        try:
            with urllib.request.urlopen(base+"/", timeout=20) as resp:
                if resp.status != 200: g["serves"]=False; g["persists"]=False; return g
                html = resp.read().decode(errors="replace")
        except Exception:
            g["serves"]=False; g["persists"]=False; return g
        g["serves"] = ('<div id="tree"' in html) and ('htmx' in html.lower())
        if not g["serves"]:
            g["persists"]=False; return g
        # POST a location (HTMX-style: expects 200 + fragment, not a redirect)
        data = urllib.parse.urlencode({"name":"BenchLoc","parent_id":""}).encode()
        try:
            req = urllib.request.Request(base+"/", data=data, method="POST")
            with urllib.request.urlopen(req, timeout=20) as resp:
                post_body = resp.read().decode(errors="replace")
        except Exception:
            g["persists"]=False; return g
        g["persists"] = ("BenchLoc" in post_body)
        if g["persists"] and os.path.exists(dbpath):
            try:
                import sqlite3
                conn = sqlite3.connect(dbpath)
                cur = conn.execute("SELECT count(*) FROM locations WHERE name='BenchLoc'")
                g["persists"] = cur.fetchone()[0] > 0
                conn.close()
            except Exception:
                pass
        return g
    finally:
        proc.send_signal(signal.SIGTERM)
        try: proc.wait(timeout=5)
        except subprocess.TimeoutExpired: proc.kill()
        for ext in ("", "-wal", "-shm"):
            p = dbpath+ext
            if os.path.exists(p):
                try: os.remove(p)
                except: pass

def validate(work_dir):
    _materialize(REFERENCE, dest=work_dir)
    r = _run("go mod tidy", work_dir)
    print("go mod tidy:", r[:120])
    b = _run("go build ./...", work_dir)
    print(f"build: {'OK' if b.startswith('exit=0') else 'FAIL'}\n{b[-300:]}")
    t = _run("go test ./...", work_dir)
    print(f"test:  {'OK' if t.startswith('exit=0') else 'FAIL'}\n{t[-300:]}")
    g = gates(work_dir)
    print(f"gates: {g}")
    ok = all(g.values())
    print(f"-> {'OK' if ok else 'BAD'}")
    return ok

# Allow `python go_htmx.py` to run validate directly
if __name__ == "__main__":
    import sys
    _work_root = os.path.join(HERE, "..", "..", "..", "work")
    wd = os.path.join(_work_root, "multitask_validate_go_htmx")
    validate(wd)