Add the conversation fingerprint and the v1 config keys
Implemented-By: OpenCode session (model recorded in docs/implementer-log.md)
This commit is contained in:
@@ -15,18 +15,25 @@ import (
|
||||
"os"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/BurntSushi/toml"
|
||||
)
|
||||
|
||||
// Duration is a time.Duration that TOML reads from a string such as "60s" or
|
||||
// "30m".
|
||||
// Duration is a time.Duration that TOML reads from a string such as "60s",
|
||||
// "30m", or "7d" (an integer number of days).
|
||||
type Duration struct{ time.Duration }
|
||||
|
||||
// UnmarshalText implements encoding.TextUnmarshaler via time.ParseDuration.
|
||||
// UnmarshalText implements encoding.TextUnmarshaler. It accepts the "Nd" form
|
||||
// — an integer number of days, so "7d" is 7 × 24h — in addition to
|
||||
// time.ParseDuration syntax.
|
||||
func (d *Duration) UnmarshalText(text []byte) error {
|
||||
if days, ok := parseDays(text); ok {
|
||||
d.Duration = days
|
||||
return nil
|
||||
}
|
||||
dt, err := time.ParseDuration(string(text))
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -35,6 +42,22 @@ func (d *Duration) UnmarshalText(text []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// dayPattern matches a run of digits followed by "d", e.g. "7d".
|
||||
var dayPattern = regexp.MustCompile(`^[0-9]+d$`)
|
||||
|
||||
// parseDays reports whether text is the "Nd" day form and returns that many
|
||||
// hours. The regex guarantees the prefix is a base-10 integer.
|
||||
func parseDays(text []byte) (time.Duration, bool) {
|
||||
if !dayPattern.MatchString(string(text)) {
|
||||
return 0, false
|
||||
}
|
||||
n, err := strconv.Atoi(string(text[:len(text)-1]))
|
||||
if err != nil {
|
||||
return 0, false
|
||||
}
|
||||
return time.Duration(n) * 24 * time.Hour, true
|
||||
}
|
||||
|
||||
// Model is the per-model tuning carried by a host entry.
|
||||
type Model struct {
|
||||
Parallel int `toml:"parallel"`
|
||||
@@ -58,6 +81,9 @@ type Config struct {
|
||||
Listen string `toml:"listen"`
|
||||
PollInterval Duration `toml:"poll_interval"`
|
||||
QueueMax int `toml:"queue_max"`
|
||||
DB string `toml:"db"`
|
||||
LeaseIdle Duration `toml:"lease_idle"`
|
||||
Retention Duration `toml:"retention"`
|
||||
Hosts map[string]Host `toml:"hosts"`
|
||||
Routes map[string]Route `toml:"routes"`
|
||||
}
|
||||
@@ -76,6 +102,13 @@ const (
|
||||
DefaultPollInterval = 60 * time.Second
|
||||
DefaultQueueMax = 8
|
||||
MinPollInterval = time.Second
|
||||
|
||||
DefaultDB = "crossbar.db"
|
||||
DefaultLeaseIdle = 30 * time.Minute
|
||||
DefaultRetention = 180 * 24 * time.Hour
|
||||
|
||||
MinLeaseIdle = time.Minute
|
||||
MinRetention = 24 * time.Hour
|
||||
)
|
||||
|
||||
var routeName = regexp.MustCompile(`^[a-z0-9][a-z0-9-]*$`)
|
||||
@@ -109,6 +142,15 @@ func Parse(r io.Reader) (*Config, error) {
|
||||
return nil, &Error{Field: keys[0], Msg: "unknown key"}
|
||||
}
|
||||
|
||||
if !md.IsDefined("db") {
|
||||
c.DB = DefaultDB
|
||||
}
|
||||
if !md.IsDefined("lease_idle") {
|
||||
c.LeaseIdle.Duration = DefaultLeaseIdle
|
||||
}
|
||||
if !md.IsDefined("retention") {
|
||||
c.Retention.Duration = DefaultRetention
|
||||
}
|
||||
if c.PollInterval.Duration == 0 {
|
||||
c.PollInterval.Duration = DefaultPollInterval
|
||||
}
|
||||
@@ -152,6 +194,15 @@ func (c *Config) validate() *Error {
|
||||
if e := c.checkQueue(); e != nil {
|
||||
return e
|
||||
}
|
||||
if e := c.checkDB(); e != nil {
|
||||
return e
|
||||
}
|
||||
if e := c.checkLeaseIdle(); e != nil {
|
||||
return e
|
||||
}
|
||||
if e := c.checkRetention(); e != nil {
|
||||
return e
|
||||
}
|
||||
if e := c.checkHosts(); e != nil {
|
||||
return e
|
||||
}
|
||||
@@ -193,6 +244,27 @@ func (c *Config) checkQueue() *Error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Config) checkDB() *Error {
|
||||
if c.DB == "" {
|
||||
return &Error{Field: "db", Msg: "required"}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Config) checkLeaseIdle() *Error {
|
||||
if c.LeaseIdle.Duration < MinLeaseIdle {
|
||||
return &Error{Field: "lease_idle", Msg: "must be at least 1m"}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Config) checkRetention() *Error {
|
||||
if c.Retention.Duration < MinRetention {
|
||||
return &Error{Field: "retention", Msg: "must be at least 1d"}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Config) checkHosts() *Error {
|
||||
if len(c.Hosts) == 0 {
|
||||
return &Error{Field: "hosts", Msg: "at least one required"}
|
||||
|
||||
@@ -81,7 +81,7 @@ func TestBadFiles(t *testing.T) {
|
||||
{"bad-listen.toml", "listen"},
|
||||
{"bad-unknown-host.toml", "routes.r.hosts"},
|
||||
{"bad-default-model.toml", "routes.r.default_model"},
|
||||
{"bad-unknown-key.toml", "lease_idle"},
|
||||
{"bad-unknown-key.toml", "bogus_key"},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.file, func(t *testing.T) {
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
package config_test
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.wntrmute.dev/kyle/crossbar/internal/config"
|
||||
)
|
||||
|
||||
const v1Base = `
|
||||
listen = "127.0.0.1:1"
|
||||
[hosts.a]
|
||||
base_url = "http://a:1"
|
||||
models = { "m" = { } }
|
||||
[routes.r]
|
||||
hosts = ["a"]
|
||||
`
|
||||
|
||||
func TestV1Defaults(t *testing.T) {
|
||||
c, err := config.Parse(strings.NewReader(v1Base))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if c.DB != "crossbar.db" {
|
||||
t.Errorf("DB default = %q", c.DB)
|
||||
}
|
||||
if c.LeaseIdle.Duration != 30*time.Minute {
|
||||
t.Errorf("LeaseIdle default = %v", c.LeaseIdle.Duration)
|
||||
}
|
||||
if c.Retention.Duration != 180*24*time.Hour {
|
||||
t.Errorf("Retention default = %v", c.Retention.Duration)
|
||||
}
|
||||
}
|
||||
|
||||
func TestV1Values(t *testing.T) {
|
||||
c, err := config.Parse(strings.NewReader(`
|
||||
db = "/var/lib/crossbar/crossbar.db"
|
||||
lease_idle = "45m"
|
||||
retention = "30d"
|
||||
` + v1Base))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if c.DB != "/var/lib/crossbar/crossbar.db" || c.LeaseIdle.Duration != 45*time.Minute || c.Retention.Duration != 30*24*time.Hour {
|
||||
t.Errorf("got db %q idle %v retention %v", c.DB, c.LeaseIdle.Duration, c.Retention.Duration)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDurationAcceptsDays(t *testing.T) {
|
||||
var d config.Duration
|
||||
for _, tc := range []struct {
|
||||
in string
|
||||
want time.Duration
|
||||
}{
|
||||
{"1d", 24 * time.Hour}, {"7d", 7 * 24 * time.Hour}, {"90m", 90 * time.Minute}, {"2h30m", 150 * time.Minute},
|
||||
} {
|
||||
if err := d.UnmarshalText([]byte(tc.in)); err != nil || d.Duration != tc.want {
|
||||
t.Errorf("UnmarshalText(%q) = %v %v, want %v", tc.in, d.Duration, err, tc.want)
|
||||
}
|
||||
}
|
||||
for _, bad := range []string{"1.5d", "d", "3 days", "1d2h"} {
|
||||
if err := d.UnmarshalText([]byte(bad)); err == nil {
|
||||
t.Errorf("UnmarshalText(%q) must fail", bad)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestV1Validation(t *testing.T) {
|
||||
for _, tc := range []struct{ name, text, field string }{
|
||||
{"empty db", "db = \"\"\n" + v1Base, "db"},
|
||||
{"lease_idle too short", "lease_idle = \"10s\"\n" + v1Base, "lease_idle"},
|
||||
{"retention too short", "retention = \"12h\"\n" + v1Base, "retention"},
|
||||
} {
|
||||
_, err := config.Parse(strings.NewReader(tc.text))
|
||||
e, ok := config.IsError(err)
|
||||
if !ok || e.Field != tc.field {
|
||||
t.Errorf("%s: %v, want *Error on %s", tc.name, err, tc.field)
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
listen = "127.0.0.1:7777"
|
||||
lease_idle = "30m"
|
||||
bogus_key = 1
|
||||
|
||||
[hosts.alpha]
|
||||
base_url = "http://alpha.example:11434"
|
||||
|
||||
Reference in New Issue
Block a user