Files
Cyrene_v2/plugins/tools/basic/calculator_test.go
T

114 lines
3.3 KiB
Go

package basic
import (
"errors"
"strings"
"testing"
)
// TestCalculator_Evaluate_Normal covers the arithmetic grammar.
func TestCalculator_Evaluate_Normal(t *testing.T) {
t.Parallel()
c := calculator{}
cases := []struct {
expr string
want float64
}{
{expr: "1+2*3", want: 7},
{expr: "(1+2)*3", want: 9},
{expr: "2^10", want: 1024},
{expr: "2^3^2", want: 512}, // right-associative
{expr: "-2^2", want: -4}, // power binds tighter than unary minus
{expr: "2^-3", want: 0.125}, // negative exponent
{expr: "sqrt(16)", want: 4},
{expr: "sqrt(2)+1", want: 2.414213562373095},
{expr: "10/4", want: 2.5},
{expr: " ( 5 + 3 ) * 2 ", want: 16},
}
for _, tc := range cases {
tc := tc
t.Run(tc.expr, func(t *testing.T) {
t.Parallel()
got, err := run(t, c, map[string]any{"expression": tc.expr})
assertNoErr(t, err)
assertApprox(t, fieldFloat(t, mapResult(t, got), "result"), tc.want)
})
}
}
// TestCalculator_Evaluate_Errors covers invalid expressions and missing/typed args.
func TestCalculator_Evaluate_Errors(t *testing.T) {
t.Parallel()
c := calculator{}
byExpression := []struct {
name string
args map[string]any
}{
{name: "missing expression", args: map[string]any{}},
{name: "non-string expression", args: map[string]any{"expression": 42}},
{name: "division by zero", args: map[string]any{"expression": "1/0"}},
{name: "trailing operator", args: map[string]any{"expression": "1+"}},
{name: "bare identifier", args: map[string]any{"expression": "x+1"}},
{name: "unknown function", args: map[string]any{"expression": "sqrt(16"}},
{name: "negative sqrt", args: map[string]any{"expression": "sqrt(-1)"}},
{name: "empty string", args: map[string]any{"expression": ""}},
{name: "non-ASCII injection", args: map[string]any{"expression": "1+2;DROP"}},
}
for _, tc := range byExpression {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
_, err := run(t, c, tc.args)
if err == nil {
t.Fatalf("want error, got nil")
}
if !errors.Is(err, ErrInvalidArgument) && !errors.Is(err, ErrEvaluation) {
t.Fatalf("err %v wraps neither ErrInvalidArgument nor ErrEvaluation", err)
}
})
}
}
// TestCalculator_InjectionRejected ensures expression injection is refused.
func TestCalculator_InjectionRejected(t *testing.T) {
t.Parallel()
c := calculator{}
payloads := []string{
"os.system('id')",
"process.env.HOME",
`__import__("os")`,
"1+2*3;rm -rf /",
"`id`",
"eval(1)",
"1 && 2",
"len(open('x'))",
}
for _, p := range payloads {
p := p
t.Run(p, func(t *testing.T) {
t.Parallel()
_, err := run(t, c, map[string]any{"expression": p})
assertIs(t, err, ErrEvaluation)
})
}
// The expression length bound rejects a long payload.
large := strings.Repeat("1+", 200) + "1"
_, err := run(t, c, map[string]any{"expression": large})
assertIs(t, err, ErrEvaluation)
}
// TestCalculator_Schema verifies the JSON Schema contract.
func TestCalculator_Schema(t *testing.T) {
t.Parallel()
p, err := calculator{}.Parameters()
assertNoErr(t, err)
reqs := p["required"].([]any)
if len(reqs) != 1 || reqs[0] != "expression" {
t.Fatalf("required = %v, want [expression]", reqs)
}
props := p["properties"].(map[string]any)
if got := props["expression"].(map[string]any)["type"]; got != "string" {
t.Fatalf("expression type = %v, want string", got)
}
}