Merge origin/master into v1: corrected TestPinAndUnpin, task 02 replacement fixtures

This commit is contained in:
2026-09-25 04:24:55 -07:00
6 changed files with 242 additions and 4 deletions
+5 -2
View File
@@ -21,8 +21,11 @@ implementing it one task at a time.
## Files you must never edit ## Files you must never edit
- `PLAN.md`, `docs/plans/`, `AGENTS.md` - `PLAN.md`, `docs/plans/`, `AGENTS.md`
- Anything a task told you to copy from `docs/plans/**/_files/`: tests, testdata, `Makefile`, - Anything a task told you to copy from `docs/plans/**/_files/` — in this plan **or any earlier
scripts, `example.toml`, `cmd/fakeupstream`. If a copied test fails, your code is wrong. one** — stays protected: tests, testdata, `Makefile`,
scripts, `example.toml`, `cmd/fakeupstream`. If a copied test fails, your code is wrong. If a
copied test can no longer be right because the new task changes what it tested, that is the
owner's error: stop and report it; the owner hands over the replacement.
## Code rules ## Code rules
+2
View File
@@ -20,6 +20,8 @@ requests fall back to the route-level lease (task 04).
## Files ## Files
- Copy: `internal/fingerprint/fingerprint_test.go`, `internal/config/config_v1_test.go` - Copy: `internal/fingerprint/fingerprint_test.go`, `internal/config/config_v1_test.go`
- Copy (**replaces** v0's): `internal/config/config_test.go`, `internal/config/testdata/bad-unknown-key.toml`
(v0's unknown-key example was `lease_idle`, which this task makes valid; the replacements use `bogus_key`)
- Create: `internal/fingerprint/fingerprint.go` - Create: `internal/fingerprint/fingerprint.go`
- Modify: `internal/config/config.go`, `docs/implementer-log.md` - Modify: `internal/config/config.go`, `docs/implementer-log.md`
+16
View File
@@ -67,3 +67,19 @@ tools/run-plan.sh docs/plans/v1 # from a clean checkout on master
not edit. Ornith diagnosed it (gofmt on its own code was clean) and did not touch them. not edit. Ornith diagnosed it (gofmt on its own code was clean) and did not touch them.
Owner's fault (the compile check ran `go vet`, not `gofmt`, on the given files). Fixed by Owner's fault (the compile check ran `go vet`, not `gofmt`, on the given files). Fixed by
formatting the given files; the plan checklist now includes `gofmt -l docs/` before handover. formatting the given files; the plan checklist now includes `gofmt -l docs/` before handover.
Ornith stopped correctly (a `stopped` row, code left in the tree, only the log committed);
a second session was told where the first had stopped and finished steps 6–7 without
starting over — the boxmaker precedent (`M3a/19`). The driver was then resumed from task 02.
- 2026-09-25, task 02: v0's given `testdata/bad-unknown-key.toml` used `lease_idle` as its
unknown-key example, and v1 makes `lease_idle` a real key, so v0's `TestBadFiles` broke — the
task did not hand over replacements for the two v0 given files (tip T19). Ornith changed the
example to `bogus_key` in both files and logged it in Deviations; the content is right, but the
files were protected and the rule was to stop. Owner's fault for the conflict; the model's
deviation is noted. The corrected files now sit in `_files/internal/config/` as the reference
copies for the reviewer's byte-exact check.
- 2026-09-25, task 04: my `TestPinAndUnpin` asserted the pin event at exactly `len-3` and the
release event at `len-1`, but the task's own rules make acquires under a pin record events too,
so a faithful implementation produces `[new pin pin new new release]` and the assertion cannot
hold. Ornith spent its first ten minutes puzzling over exactly that. Test fault (mine): the
assertion now checks order and content (a pin event naming beta, followed later by a release),
not positions.
@@ -0,0 +1,191 @@
package config_test
import (
"fmt"
"path/filepath"
"strings"
"testing"
"time"
"git.wntrmute.dev/kyle/crossbar/internal/config"
)
func TestGoodFile(t *testing.T) {
c, err := config.Load(filepath.Join("testdata", "good.toml"))
if err != nil {
t.Fatalf("Load: %v", err)
}
if c.Listen != "100.64.0.9:7777" {
t.Errorf("Listen = %q", c.Listen)
}
if c.PollInterval.Duration != 5*time.Second {
t.Errorf("PollInterval = %v", c.PollInterval.Duration)
}
if c.QueueMax != 4 {
t.Errorf("QueueMax = %d", c.QueueMax)
}
alpha := c.Hosts["alpha"]
if alpha.BaseURL != "http://alpha.example:11434" {
t.Errorf("trailing slash not stripped: %q", alpha.BaseURL)
}
if alpha.Weight != 2 {
t.Errorf("alpha.Weight = %v", alpha.Weight)
}
if alpha.Models["ornith-1.5-35b-a3b"].Parallel != 4 || alpha.Models["small-9b"].Parallel != 6 {
t.Errorf("alpha.Models = %+v", alpha.Models)
}
beta := c.Hosts["beta"]
if beta.Weight != 1 {
t.Errorf("beta.Weight default = %v, want 1", beta.Weight)
}
if beta.Models["ornith-1.5-35b-a3b"].Parallel != 1 {
t.Errorf("beta parallel default = %d, want 1", beta.Models["ornith-1.5-35b-a3b"].Parallel)
}
r := c.Routes["opencode-a"]
if len(r.Hosts) != 2 || r.Hosts[0] != "alpha" || r.Hosts[1] != "beta" {
t.Errorf("route hosts = %v", r.Hosts)
}
if r.DefaultModel != "ornith-1.5-35b-a3b" {
t.Errorf("DefaultModel = %q", r.DefaultModel)
}
if c.Routes["hermes-x"].DefaultModel != "" {
t.Errorf("hermes-x DefaultModel should be empty")
}
if !c.Serves("alpha", "small-9b") || c.Serves("beta", "small-9b") || c.Serves("nope", "m") {
t.Errorf("Serves is wrong")
}
}
func TestDefaults(t *testing.T) {
c, err := config.Parse(strings.NewReader(`
listen = "127.0.0.1:1"
[hosts.a]
base_url = "http://a:1"
models = { "m" = { } }
[routes.r]
hosts = ["a"]
`))
if err != nil {
t.Fatalf("Parse: %v", err)
}
if c.PollInterval.Duration != config.DefaultPollInterval {
t.Errorf("PollInterval default = %v", c.PollInterval.Duration)
}
if c.QueueMax != config.DefaultQueueMax {
t.Errorf("QueueMax default = %d", c.QueueMax)
}
}
func TestBadFiles(t *testing.T) {
cases := []struct{ file, field string }{
{"bad-listen.toml", "listen"},
{"bad-unknown-host.toml", "routes.r.hosts"},
{"bad-default-model.toml", "routes.r.default_model"},
{"bad-unknown-key.toml", "bogus_key"},
}
for _, tc := range cases {
t.Run(tc.file, func(t *testing.T) {
_, err := config.Load(filepath.Join("testdata", tc.file))
if err == nil {
t.Fatalf("want error")
}
e, ok := config.IsError(err)
if !ok {
t.Fatalf("want *config.Error, got %T: %v", err, err)
}
if e.Field != tc.field {
t.Errorf("Field = %q, want %q (%v)", e.Field, tc.field, err)
}
if !strings.HasPrefix(err.Error(), "config: "+tc.field+": ") {
t.Errorf("Error() = %q", err.Error())
}
})
}
}
func TestBadValues(t *testing.T) {
base := `
listen = %q
poll_interval = %q
[hosts.a]
base_url = %q
weight = %v
models = { "m" = { parallel = %d } }
[routes.%s]
hosts = ["a"]
`
cases := []struct {
name string
listen, poll, url, route string
weight float64
parallel int
field string
}{
{"empty listen", "", "5s", "http://a:1", "r", 1, 1, "listen"},
{"no port", "127.0.0.1", "5s", "http://a:1", "r", 1, 1, "listen"},
{"v6 any", "[::]:7", "5s", "http://a:1", "r", 1, 1, "listen"},
{"poll too short", "127.0.0.1:7", "500ms", "http://a:1", "r", 1, 1, "poll_interval"},
{"ftp url", "127.0.0.1:7", "5s", "ftp://a:1", "r", 1, 1, "hosts.a.base_url"},
{"no host", "127.0.0.1:7", "5s", "http://", "r", 1, 1, "hosts.a.base_url"},
{"query", "127.0.0.1:7", "5s", "http://a:1/v1?x=1", "r", 1, 1, "hosts.a.base_url"},
{"negative weight", "127.0.0.1:7", "5s", "http://a:1", "r", -1, 1, "hosts.a.weight"},
{"negative parallel", "127.0.0.1:7", "5s", "http://a:1", "r", 1, -2, "hosts.a.models.m.parallel"},
{"route name", "127.0.0.1:7", "5s", "http://a:1", "Bad_Name", 1, 1, "routes.Bad_Name"},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
text := fmt.Sprintf(base, tc.listen, tc.poll, tc.url, tc.weight, tc.parallel, tc.route)
_, err := config.Parse(strings.NewReader(text))
if err == nil {
t.Fatalf("want error for %s", tc.name)
}
e, ok := config.IsError(err)
if !ok {
t.Fatalf("want *config.Error, got %T: %v", err, err)
}
if e.Field != tc.field {
t.Errorf("Field = %q, want %q (%v)", e.Field, tc.field, err)
}
})
}
}
func TestMissingSections(t *testing.T) {
for _, tc := range []struct{ name, text, field string }{
{"no hosts", "listen = \"127.0.0.1:7\"\n[routes.r]\nhosts = [\"a\"]\n", "hosts"},
{"no routes", "listen = \"127.0.0.1:7\"\n[hosts.a]\nbase_url = \"http://a:1\"\nmodels = { \"m\" = { } }\n", "routes"},
{"host without models", "listen = \"127.0.0.1:7\"\n[hosts.a]\nbase_url = \"http://a:1\"\n[routes.r]\nhosts = [\"a\"]\n", "hosts.a.models"},
{"route without hosts", "listen = \"127.0.0.1:7\"\n[hosts.a]\nbase_url = \"http://a:1\"\nmodels = { \"m\" = { } }\n[routes.r]\n", "routes.r.hosts"},
{"host twice", "listen = \"127.0.0.1:7\"\n[hosts.a]\nbase_url = \"http://a:1\"\nmodels = { \"m\" = { } }\n[routes.r]\nhosts = [\"a\", \"a\"]\n", "routes.r.hosts"},
} {
t.Run(tc.name, func(t *testing.T) {
_, err := config.Parse(strings.NewReader(tc.text))
e, ok := config.IsError(err)
if !ok {
t.Fatalf("want *config.Error, got %v", err)
}
if e.Field != tc.field {
t.Errorf("Field = %q, want %q", e.Field, tc.field)
}
})
}
}
func TestNotTOML(t *testing.T) {
_, err := config.Parse(strings.NewReader("listen = [unterminated"))
if err == nil {
t.Fatal("want error")
}
if _, ok := config.IsError(err); ok {
t.Errorf("a syntax error is not a validation Error")
}
if !strings.HasPrefix(err.Error(), "config: ") {
t.Errorf("Error() = %q", err.Error())
}
}
func TestMissingFile(t *testing.T) {
if _, err := config.Load(filepath.Join("testdata", "does-not-exist.toml")); err == nil {
t.Fatal("want error")
}
}
@@ -0,0 +1,9 @@
listen = "127.0.0.1:7777"
bogus_key = 1
[hosts.alpha]
base_url = "http://alpha.example:11434"
models = { "m" = { } }
[routes.r]
hosts = ["alpha"]
@@ -217,9 +217,26 @@ func TestPinAndUnpin(t *testing.T) {
if host, _, _ := tbl.Acquire(k, []string{"alpha", "beta"}, t0.Add(4*time.Minute)); host != "beta" { if host, _, _ := tbl.Acquire(k, []string{"alpha", "beta"}, t0.Add(4*time.Minute)); host != "beta" {
t.Errorf("after unpin the existing lease (on beta) simply continues: %q", host) t.Errorf("after unpin the existing lease (on beta) simply continues: %q", host)
} }
if got := p.reasons(); got[len(got)-3] != store.ReasonPin || got[len(got)-1] != store.ReasonRelease { // Events: a pin event naming beta must exist, and the unpin's release event must come after it.
t.Errorf("events = %v, want a pin event and a release event", got) // Acquires under the pin may record their own events in between; their number is not fixed here.
got := p.reasons()
pinAt, releaseAt := -1, -1
for i, r := range got {
if r == store.ReasonPin && pinAt < 0 {
pinAt = i
}
if r == store.ReasonRelease {
releaseAt = i
}
} }
if pinAt < 0 || releaseAt < pinAt {
t.Errorf("events = %v, want a pin event followed later by a release event", got)
}
p.mu.Lock()
if pinAt >= 0 && p.events[pinAt].ToHost != "beta" {
t.Errorf("pin event = %+v, want ToHost beta", p.events[pinAt])
}
p.mu.Unlock()
} }
func TestDrainKeepsExistingRefusesNew(t *testing.T) { func TestDrainKeepsExistingRefusesNew(t *testing.T) {