test: reference fixture app implementing the contract

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Paul Campbell pushed by claude agent, operated by paul@keymail.dev 1a5d6e13b17d8f90c47f7d8f532ebb852d40cddc parent 233cace
5 files changed, +519 −0
  • harness/go.mod +3 −0
  • harness/refapp/app.js +57 −0
  • harness/refapp/main.go +251 −0
  • harness/refapp/page.html.tmpl +82 −0
  • harness/refapp/refapp_test.go +126 −0
diff --git a/harness/go.mod b/harness/go.mod
new file mode 100644
index 0000000..d07f8b2
--- /dev/null
+++ b/harness/go.mod
@@ -0,0 +1,3 @@
+module bakeoff/harness
+
+go 1.26
diff --git a/harness/refapp/app.js b/harness/refapp/app.js
new file mode 100644
index 0000000..88e4fbc
--- /dev/null
+++ b/harness/refapp/app.js
@@ -0,0 +1,57 @@
+// Progressive enhancement for the reference fixture: every form in the app
+// is submitted with fetch, and the server's re-rendered .todoapp replaces
+// the current one. Editing in place is client-side until it saves.
+const FETCH = { 'X-Requested-With': 'fetch' };
+
+async function send(form) {
+ const res = await fetch(form.action, {
+ method: 'POST',
+ headers: FETCH,
+ body: new URLSearchParams(new FormData(form)),
+ });
+ const doc = new DOMParser().parseFromString(await res.text(), 'text/html');
+ document.querySelector('.todoapp').replaceWith(doc.querySelector('.todoapp'));
+}
+
+document.addEventListener('submit', async (e) => {
+ const form = e.target;
+ if (!form.closest('.todoapp')) return;
+ e.preventDefault();
+ // Leave editing mode first, so the focusout that follows doesn't save twice.
+ form.closest('li')?.classList.remove('editing');
+ const adding = form.querySelector('.new-todo');
+ await send(form);
+ if (adding) document.querySelector('.new-todo').focus();
+});
+
+document.addEventListener('change', (e) => {
+ if (e.target.matches('.toggle, .toggle-all')) e.target.form.requestSubmit();
+});
+
+document.addEventListener('dblclick', (e) => {
+ const label = e.target.closest('.todo-list label');
+ if (!label) return;
+ const li = label.closest('li');
+ li.classList.add('editing');
+ const input = li.querySelector('.edit');
+ input.focus();
+ input.setSelectionRange(input.value.length, input.value.length);
+});
+
+document.addEventListener('keydown', (e) => {
+ if (!e.target.matches('.edit')) return;
+ const li = e.target.closest('li');
+ if (e.key === 'Escape') {
+ e.target.value = li.querySelector('label').textContent;
+ li.classList.remove('editing');
+ }
+});
+
+document.addEventListener('focusout', (e) => {
+ if (!e.target.matches('.edit')) return;
+ const li = e.target.closest('li');
+ if (li.classList.contains('editing')) {
+ li.classList.remove('editing');
+ e.target.form.requestSubmit();
+ }
+});
diff --git a/harness/refapp/main.go b/harness/refapp/main.go
new file mode 100644
index 0000000..3a695d6
--- /dev/null
+++ b/harness/refapp/main.go
@@ -0,0 +1,251 @@
+// Command refapp is the bakeoff's reference fixture: a minimal, in-memory
+// implementation of CONTRACT.md used only to prove the behaviour suite and
+// the harness before any builder starts. It is not a contestant, and
+// builders never see it.
+package main
+
+import (
+ "crypto/rand"
+ _ "embed"
+ "encoding/hex"
+ "flag"
+ "html/template"
+ "log"
+ "net"
+ "net/http"
+ "os"
+ "path/filepath"
+ "strconv"
+ "strings"
+ "sync"
+)
+
+//go:embed page.html.tmpl
+var pageSrc string
+
+//go:embed app.js
+var appJS []byte
+
+var page = template.Must(template.New("page").Parse(pageSrc))
+
+const cookieName = "todo"
+
+type item struct {
+ ID int
+ Title string
+ Completed bool
+}
+
+type list struct {
+ next int
+ items []item
+}
+
+type app struct {
+ origin string
+ shared string
+ mu sync.Mutex
+ lists map[string]*list
+}
+
+// NewApp returns the reference app's handler. origin is the app's own
+// origin for the Origin check; shared is the directory holding base.css
+// and index.css.
+func NewApp(origin, shared string) http.Handler {
+ a := &app{origin: origin, shared: shared, lists: map[string]*list{}}
+ mux := http.NewServeMux()
+ mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, _ *http.Request) { w.Write([]byte("ok")) })
+ mux.HandleFunc("GET /api/version", func(w http.ResponseWriter, _ *http.Request) { w.Write([]byte("refapp")) })
+ mux.HandleFunc("GET /static/{file}", a.static)
+ mux.HandleFunc("GET /{$}", a.show("all"))
+ mux.HandleFunc("GET /active", a.show("active"))
+ mux.HandleFunc("GET /completed", a.show("completed"))
+ mux.HandleFunc("POST /todos", a.act(func(l *list, r *http.Request) int {
+ if t := strings.TrimSpace(r.FormValue("title")); t != "" {
+ l.next++
+ l.items = append(l.items, item{ID: l.next, Title: t})
+ }
+ return 0
+ }))
+ mux.HandleFunc("POST /todos/toggle-all", a.act(func(l *list, _ *http.Request) int {
+ all := true
+ for _, it := range l.items {
+ all = all && it.Completed
+ }
+ for i := range l.items {
+ l.items[i].Completed = !all
+ }
+ return 0
+ }))
+ mux.HandleFunc("POST /todos/clear-completed", a.act(func(l *list, _ *http.Request) int {
+ kept := l.items[:0]
+ for _, it := range l.items {
+ if !it.Completed {
+ kept = append(kept, it)
+ }
+ }
+ l.items = kept
+ return 0
+ }))
+ mux.HandleFunc("POST /todos/{id}/{op}", a.act(func(l *list, r *http.Request) int {
+ i := l.find(r.PathValue("id"))
+ if i < 0 {
+ return http.StatusNotFound
+ }
+ switch r.PathValue("op") {
+ case "toggle":
+ l.items[i].Completed = !l.items[i].Completed
+ case "edit":
+ if t := strings.TrimSpace(r.FormValue("title")); t != "" {
+ l.items[i].Title = t
+ } else {
+ l.items = append(l.items[:i], l.items[i+1:]...)
+ }
+ case "destroy":
+ l.items = append(l.items[:i], l.items[i+1:]...)
+ default:
+ return http.StatusNotFound
+ }
+ return 0
+ }))
+ return mux
+}
+
+func (l *list) find(id string) int {
+ n, err := strconv.Atoi(id)
+ if err != nil {
+ return -1
+ }
+ for i, it := range l.items {
+ if it.ID == n {
+ return i
+ }
+ }
+ return -1
+}
+
+func (a *app) static(w http.ResponseWriter, r *http.Request) {
+ switch f := r.PathValue("file"); f {
+ case "base.css", "index.css":
+ w.Header().Set("Content-Type", "text/css; charset=utf-8")
+ http.ServeFile(w, r, filepath.Join(a.shared, f))
+ case "app.js":
+ w.Header().Set("Content-Type", "text/javascript; charset=utf-8")
+ w.Write(appJS)
+ default:
+ http.NotFound(w, r)
+ }
+}
+
+// listFor returns the caller's list, minting a fresh one (and its cookie)
+// when the cookie is missing or unknown. Caller holds a.mu.
+func (a *app) listFor(w http.ResponseWriter, r *http.Request) *list {
+ if c, err := r.Cookie(cookieName); err == nil {
+ if l, ok := a.lists[c.Value]; ok {
+ return l
+ }
+ }
+ b := make([]byte, 16)
+ rand.Read(b)
+ key := hex.EncodeToString(b)
+ l := &list{}
+ a.lists[key] = l
+ http.SetCookie(w, &http.Cookie{Name: cookieName, Value: key, Path: "/", HttpOnly: true,
+ SameSite: http.SameSiteLaxMode, Secure: strings.HasPrefix(a.origin, "https://")})
+ return l
+}
+
+func filterPath(f string) string {
+ switch f {
+ case "active", "completed":
+ return "/" + f
+ }
+ return "/"
+}
+
+func (a *app) act(f func(*list, *http.Request) int) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ if o := r.Header.Get("Origin"); o != "" && o != a.origin {
+ http.Error(w, "cross-site request refused", http.StatusForbidden)
+ return
+ }
+ r.ParseForm()
+ a.mu.Lock()
+ status := f(a.listFor(w, r), r)
+ a.mu.Unlock()
+ if status != 0 {
+ http.Error(w, http.StatusText(status), status)
+ return
+ }
+ filter := r.FormValue("filter")
+ if r.Header.Get("X-Requested-With") == "fetch" {
+ a.render(w, r, filter)
+ return
+ }
+ http.Redirect(w, r, filterPath(filter), http.StatusSeeOther)
+ }
+}
+
+func (a *app) show(filter string) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) { a.render(w, r, filter) }
+}
+
+type view struct {
+ Filter, Path string
+ Items []item
+ Total, Left, Done int
+ AllDone bool
+ EditID int
+}
+
+func (a *app) render(w http.ResponseWriter, r *http.Request, filter string) {
+ if filter != "active" && filter != "completed" {
+ filter = "all"
+ }
+ a.mu.Lock()
+ l := a.listFor(w, r)
+ v := view{Filter: filter, Path: filterPath(filter), Total: len(l.items)}
+ for _, it := range l.items {
+ if it.Completed {
+ v.Done++
+ } else {
+ v.Left++
+ }
+ if filter == "all" || (filter == "completed") == it.Completed {
+ v.Items = append(v.Items, it)
+ }
+ }
+ a.mu.Unlock()
+ v.AllDone = v.Total > 0 && v.Left == 0
+ v.EditID, _ = strconv.Atoi(r.URL.Query().Get("edit"))
+ w.Header().Set("Content-Type", "text/html; charset=utf-8")
+ if err := page.Execute(w, v); err != nil {
+ log.Print(err)
+ }
+}
+
+func main() {
+ listen := flag.String("listen", "", "TCP address, e.g. 127.0.0.1:8080")
+ socket := flag.String("socket", "", "unix socket path")
+ flag.String("db", "", "ignored: refapp keeps data in memory")
+ shared := flag.String("shared", "shared", "directory holding base.css and index.css")
+ flag.Parse()
+ var ln net.Listener
+ var err error
+ origin := ""
+ switch {
+ case *socket != "":
+ os.Remove(*socket)
+ ln, err = net.Listen("unix", *socket)
+ origin = "https://" + os.Getenv("CARLOS_HOST")
+ case *listen != "":
+ ln, err = net.Listen("tcp", *listen)
+ origin = "http://" + *listen
+ default:
+ log.Fatal("need --listen or --socket")
+ }
+ if err != nil {
+ log.Fatal(err)
+ }
+ log.Fatal(http.Serve(ln, NewApp(origin, *shared)))
+}
diff --git a/harness/refapp/page.html.tmpl b/harness/refapp/page.html.tmpl
new file mode 100644
index 0000000..e2117f1
--- /dev/null
+++ b/harness/refapp/page.html.tmpl
@@ -0,0 +1,82 @@
+<!doctype html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <title>TodoMVC</title>
+ <link rel="stylesheet" href="/static/base.css">
+ <link rel="stylesheet" href="/static/index.css">
+ <style>.js .nojs{display:none}.nojs{font-size:12px}.todo-list .nojs[data-action=edit]{position:absolute;right:60px;top:22px}.todo-list button.nojs{position:absolute;right:100px;top:20px}</style>
+ <script>document.documentElement.classList.add('js')</script>
+ <script type="module" src="/static/app.js"></script>
+ </head>
+ <body>
+ <section class="todoapp">
+ <header class="header">
+ <h1>todos</h1>
+ <form method="post" action="/todos">
+ <input type="hidden" name="filter" value="{{.Filter}}">
+ <input class="new-todo" name="title" placeholder="What needs to be done?" autofocus>
+ </form>
+ </header>
+ {{- if .Total}}
+ <section class="main">
+ <form method="post" action="/todos/toggle-all">
+ <input type="hidden" name="filter" value="{{.Filter}}">
+ <input id="toggle-all" class="toggle-all" type="checkbox"{{if .AllDone}} checked{{end}}>
+ <label for="toggle-all">Mark all as complete</label>
+ <button class="nojs" data-action="toggle-all">Toggle all</button>
+ </form>
+ <ul class="todo-list">
+ {{- range .Items}}
+ <li data-id="{{.ID}}"{{if or .Completed (eq .ID $.EditID)}} class="{{if .Completed}}completed{{end}}{{if and .Completed (eq .ID $.EditID)}} {{end}}{{if eq .ID $.EditID}}editing{{end}}"{{end}}>
+ <div class="view">
+ <form method="post" action="/todos/{{.ID}}/toggle">
+ <input type="hidden" name="filter" value="{{$.Filter}}">
+ <input class="toggle" type="checkbox"{{if .Completed}} checked{{end}}>
+ <label>{{.Title}}</label>
+ <button class="nojs" data-action="toggle">Toggle</button>
+ </form>
+ <a class="nojs" data-action="edit" href="{{$.Path}}?edit={{.ID}}">Edit</a>
+ <form method="post" action="/todos/{{.ID}}/destroy">
+ <input type="hidden" name="filter" value="{{$.Filter}}">
+ <button class="destroy"></button>
+ </form>
+ </div>
+ <form method="post" action="/todos/{{.ID}}/edit">
+ <input type="hidden" name="filter" value="{{$.Filter}}">
+ <input class="edit" name="title" value="{{.Title}}"{{if eq .ID $.EditID}} autofocus{{end}}>
+ </form>
+ </li>
+ {{- end}}
+ </ul>
+ </section>
+ <footer class="footer">
+ <span class="todo-count"><strong>{{.Left}}</strong> {{if eq .Left 1}}item{{else}}items{{end}} left</span>
+ <ul class="filters">
+ <li>
+ <a{{if eq .Filter "all"}} class="selected"{{end}} href="/">All</a>
+ </li>
+ <li>
+ <a{{if eq .Filter "active"}} class="selected"{{end}} href="/active">Active</a>
+ </li>
+ <li>
+ <a{{if eq .Filter "completed"}} class="selected"{{end}} href="/completed">Completed</a>
+ </li>
+ </ul>
+ {{- if .Done}}
+ <form method="post" action="/todos/clear-completed">
+ <input type="hidden" name="filter" value="{{.Filter}}">
+ <button class="clear-completed">Clear completed</button>
+ </form>
+ {{- end}}
+ </footer>
+ {{- end}}
+ </section>
+ <footer class="info">
+ <p>Double-click to edit a todo</p>
+ <p>Created by <a href="https://bakeoff.bfj.oncarlos.com">the CARLOS bakeoff</a> · Reference fixture</p>
+ <p>Part of <a href="http://todomvc.com">TodoMVC</a></p>
+ </footer>
+ </body>
+</html>
diff --git a/harness/refapp/refapp_test.go b/harness/refapp/refapp_test.go
new file mode 100644
index 0000000..3715d21
--- /dev/null
+++ b/harness/refapp/refapp_test.go
@@ -0,0 +1,126 @@
+package main
+
+import (
+ "io"
+ "net/http"
+ "net/http/cookiejar"
+ "net/http/httptest"
+ "net/url"
+ "regexp"
+ "strings"
+ "testing"
+)
+
+func newTestServer(t *testing.T) *httptest.Server {
+ t.Helper()
+ srv := httptest.NewUnstartedServer(nil)
+ srv.Start()
+ srv.Config.Handler = NewApp(srv.URL, "../../shared")
+ t.Cleanup(srv.Close)
+ return srv
+}
+
+func client(t *testing.T) *http.Client {
+ t.Helper()
+ jar, _ := cookiejar.New(nil)
+ return &http.Client{
+ Jar: jar,
+ CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse },
+ }
+}
+
+func post(t *testing.T, c *http.Client, srv *httptest.Server, path string, form url.Values, origin string) *http.Response {
+ t.Helper()
+ req, _ := http.NewRequest("POST", srv.URL+path, strings.NewReader(form.Encode()))
+ req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
+ if origin != "" {
+ req.Header.Set("Origin", origin)
+ }
+ res, err := c.Do(req)
+ if err != nil {
+ t.Fatal(err)
+ }
+ res.Body.Close()
+ return res
+}
+
+func get(t *testing.T, c *http.Client, srv *httptest.Server, path string) (int, string) {
+ t.Helper()
+ res, err := c.Get(srv.URL + path)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer res.Body.Close()
+ b, _ := io.ReadAll(res.Body)
+ return res.StatusCode, string(b)
+}
+
+var liRe = regexp.MustCompile(`<li[^>]*data-id="([^"]+)"`)
+
+func ids(body string) []string {
+ var out []string
+ for _, m := range liRe.FindAllStringSubmatch(body, -1) {
+ out = append(out, m[1])
+ }
+ return out
+}
+
+func TestAddRedirectsToFilter(t *testing.T) {
+ srv := newTestServer(t)
+ c := client(t)
+ get(t, c, srv, "/")
+ res := post(t, c, srv, "/todos", url.Values{"title": {" a "}, "filter": {"active"}}, srv.URL)
+ if res.StatusCode != 303 || res.Header.Get("Location") != "/active" {
+ t.Fatalf("got %d %q, want 303 /active", res.StatusCode, res.Header.Get("Location"))
+ }
+ _, body := get(t, c, srv, "/")
+ if n := len(ids(body)); n != 1 {
+ t.Fatalf("want 1 item, got %d", n)
+ }
+ if !strings.Contains(body, "<label>a</label>") {
+ t.Fatalf("want trimmed label 'a'")
+ }
+}
+
+func TestForeignOriginForbidden(t *testing.T) {
+ srv := newTestServer(t)
+ c := client(t)
+ get(t, c, srv, "/")
+ res := post(t, c, srv, "/todos", url.Values{"title": {"x"}}, "https://evil.example")
+ if res.StatusCode != 403 {
+ t.Fatalf("got %d, want 403", res.StatusCode)
+ }
+}
+
+func TestOtherListIDNotFound(t *testing.T) {
+ srv := newTestServer(t)
+ a, b := client(t), client(t)
+ get(t, a, srv, "/")
+ get(t, b, srv, "/")
+ post(t, a, srv, "/todos", url.Values{"title": {"mine"}}, srv.URL)
+ _, body := get(t, a, srv, "/")
+ id := ids(body)[0]
+ res := post(t, b, srv, "/todos/"+id+"/toggle", nil, srv.URL)
+ if res.StatusCode != 404 {
+ t.Fatalf("got %d, want 404", res.StatusCode)
+ }
+ _, body = get(t, a, srv, "/completed")
+ if len(ids(body)) != 0 {
+ t.Fatalf("A's item was toggled by B")
+ }
+}
+
+func TestGarbledCookieGetsEmptyList(t *testing.T) {
+ srv := newTestServer(t)
+ req, _ := http.NewRequest("GET", srv.URL+"/", nil)
+ req.Header.Set("Cookie", "todo=%%%")
+ res, err := http.DefaultClient.Do(req)
+ if err != nil {
+ t.Fatal(err)
+ }
+ b, _ := io.ReadAll(res.Body)
+ res.Body.Close()
+ if res.StatusCode != 200 || len(ids(string(b))) != 0 {
+ t.Fatalf("got %d with %d items", res.StatusCode, len(ids(string(b))))
+ }
+}