#!/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)