v0.2.0 validates requests against the hosted API's limits before any network call. The JEV functions now check the same limits themselves (at most 255 labels and 10 score levels, a non-empty question), so an oversized question shows #VALUE! at once instead of Loading… followed by a failure. A test keeps the engine's limits equal to the SDK's.
- commit
- e31e05c4c678d894e728340d937c982bca4692c8
- parent
- 33ba94ae00bfe2baa213073eefdc6760e704a938
- author
- Dami <dami.etoile@gmail.com>
| M | go.mod | 2 | +- |
| M | go.sum | 2 | ++ |
| M | internal/ | 31 | ++++++++++++++++++++++++++++++ |
| M | internal/ | 29 | ++++++++++++++++++++++------ |
@@ -10,7 +10,7 @@ require (
1010 )1111 1212 require (13- github.com/FelineStateMachine/typesafe-go v0.1.0 // indirect13+ github.com/FelineStateMachine/typesafe-go v0.2.0 // indirect1414 github.com/charmbracelet/colorprofile v0.4.3 // indirect1515 github.com/charmbracelet/ultraviolet v0.0.0-20260811164956-006e29f97886 // indirect1616 github.com/charmbracelet/x/term v0.2.2 // indirect@@ -4,6 +4,8 @@ charm.land/lipgloss/v2 v2.0.6 h1:EaGKeuA8FvF+v2BT5VmZd2LoYLaMZJXA5n34th8nCIQ=
44 charm.land/lipgloss/v2 v2.0.6/go.mod h1:ipDDJNSGa1hlwDtSfW1s2/xR8Vdhbut4PXh2zEKZd0Q=55 github.com/FelineStateMachine/typesafe-go v0.1.0 h1:zJm2QvIGipgC8HiybTqeSj41EVyEjbwKL2aDX2rDPF0=66 github.com/FelineStateMachine/typesafe-go v0.1.0/go.mod h1:+8AvGJI/KbRkfIjRawUc3Y+PjmhnfmttA2m9HQg3sa8=7+github.com/FelineStateMachine/typesafe-go v0.2.0 h1:7YFrdcsuvTvBf0dhOvifZVbM8d4StrckWJC3yEH3Ydk=8+github.com/FelineStateMachine/typesafe-go v0.2.0/go.mod h1:+8AvGJI/KbRkfIjRawUc3Y+PjmhnfmttA2m9HQg3sa8=79 github.com/aymanbagabas/go-udiff v0.4.1 h1:OEIrQ8maEeDBXQDoGCbbTTXYJMYRCRO1fnodZ12Gv5o=810 github.com/aymanbagabas/go-udiff v0.4.1/go.mod h1:0L9PGwj20lrtmEMeyw4WKJ/TMyDtvAoK9bf2u/mNo3w=911 github.com/charmbracelet/colorprofile v0.4.3 h1:QPa1IWkYI+AOB+fE+mg/5/4HRMZcaXex9t5KX76i20Q=@@ -3,8 +3,10 @@ package jev
33 import (44 "context"55 "errors"6+ "fmt"67 "os"78 "path/filepath"9+ "strings"810 "testing"911 1012 typesafe "github.com/FelineStateMachine/typesafe-go"@@ -126,3 +128,32 @@ func TestAsk(t *testing.T) {
126128 t.Errorf("failure %+v", a)127129 }128130 }131+132+// The engine rejects oversized questions itself; its limits must match133+// the SDK's, which match the service.134+func TestLimitsMatchSDK(t *testing.T) {135+ s := sheet.New()136+ sheet.Remote = NewCache()137+ defer func() { sheet.Remote = nil }()138+ labels := func(n int) string {139+ out := make([]string, n)140+ for i := range out {141+ out[i] = fmt.Sprint("l", i)142+ }143+ return strings.Join(out, ",")144+ }145+ for _, tt := range []struct {146+ fn string147+ max int148+ }{{"JEV.CLASSIFY", typesafe.MaxChoiceOptions}, {"JEV.SCORE", typesafe.MaxScoreLevels}} {149+ at := sheet.Addr{}150+ s.Set(at, fmt.Sprintf(`=%s("x", "q", "%s")`, tt.fn, labels(tt.max)))151+ if !sheet.IsPending(s.Value(at)) {152+ t.Errorf("%s with %d options rejected: %+v", tt.fn, tt.max, s.Value(at))153+ }154+ s.Set(at, fmt.Sprintf(`=%s("x", "q", "%s")`, tt.fn, labels(tt.max+1)))155+ if v := s.Value(at); v != sheet.ErrValue {156+ t.Errorf("%s with %d options = %+v, want #VALUE!", tt.fn, tt.max+1, v)157+ }158+ }159+}@@ -33,6 +33,14 @@ func init() {
3333 )3434 }3535 36+// Limits the JEV service enforces (typesafe.MaxChoiceOptions and37+// MaxScoreLevels; internal/jev tests keep them in step). Checking them here38+// shows #VALUE! at once instead of a request that is bound to fail.39+const (40+ maxChoiceLabels = 25541+ maxScoreLevels = 1042+)43+3644 // ErrRemote is a question the model couldn't answer, e.g. a network3745 // failure; the context line says why.3846 var ErrRemote = Value{Kind: Error, Str: "#ERROR!"}@@ -112,6 +120,15 @@ func jevText(n Node, get lookup) (string, error) {
112120 return strings.TrimSpace(text(v)), nil113121 }114122 123+// jevQuestion reads the question, which must not be empty.124+func jevQuestion(n Node, get lookup) (string, error) {125+ q, err := jevText(n, get)126+ if err == nil && q == "" {127+ err = inputError{ErrValue}128+ }129+ return q, err130+}131+115132 // jevList reads a list argument: a range of cells, or one text of116133 // comma-separated items like "positive, negative, neutral". Blanks are117134 // skipped.@@ -150,7 +167,7 @@ func yesNoCall(args []Node, get lookup) (RemoteCall, error) {
150167 if err != nil {151168 return RemoteCall{}, err152169 }153- q, err := jevText(args[1], get)170+ q, err := jevQuestion(args[1], get)154171 if err != nil {155172 return RemoteCall{}, err156173 }@@ -170,7 +187,7 @@ func choiceCall(args []Node, get lookup) (RemoteCall, error) {
170187 if err != nil {171188 return RemoteCall{}, err172189 }173- q, err := jevText(args[1], get)190+ q, err := jevQuestion(args[1], get)174191 if err != nil {175192 return RemoteCall{}, err176193 }@@ -184,8 +201,8 @@ func choiceCall(args []Node, get lookup) (RemoteCall, error) {
184201 return RemoteCall{}, err185202 }186203 }187- if len(labels) < 2 {188- return RemoteCall{}, inputError{ErrValue} // nothing to choose between204+ if len(labels) < 2 || len(labels) > maxChoiceLabels {205+ return RemoteCall{}, inputError{ErrValue} // nothing to choose between, or too many189206 }190207 criteria := make(map[string]string, len(labels))191208 for i, l := range labels {@@ -202,7 +219,7 @@ func scoreCall(args []Node, get lookup) (RemoteCall, error) {
202219 if err != nil {203220 return RemoteCall{}, err204221 }205- q, err := jevText(args[1], get)222+ q, err := jevQuestion(args[1], get)206223 if err != nil {207224 return RemoteCall{}, err208225 }@@ -210,7 +227,7 @@ func scoreCall(args []Node, get lookup) (RemoteCall, error) {
210227 if err != nil {211228 return RemoteCall{}, err212229 }213- if len(levels) < 2 {230+ if len(levels) < 2 || len(levels) > maxScoreLevels {214231 return RemoteCall{}, inputError{ErrValue}215232 }216233 return RemoteCall{Kind: "score", State: state, Instructions: q, Criteria: levels}, nil