myxce/src/frontend/mcp-server/internal/templates_test.go
lei.chen 35416d5cf4 feat(scheduler-agent): Phase 1 write operations — submit/cancel/hold/release + templates
Add write operations to the scheduler-agent MCP server (Phase 1):

- schedulerclient: SubmitJob/CancelJob/HoldJob/ReleaseJob + JobSpec/
  SubmitJobResult/JobControlRequest/ControlResult types + doPost helper
  (HTTP 200 + envelope-code dual check). TDD: 12 new tests.
- mcp-server: 4 write tools (scheduler_submit/cancel/hold/release) with
  destructive annotations; scheduler_submit has dry_run review mode +
  general_serial (通用串行脚本) job template registry. Fix the 6 read-only
  tools' misleading destructive annotations. TDD: 14 new tests.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-01 09:54:38 +00:00

216 lines
5.9 KiB
Go

package internal
import (
"strings"
"testing"
sclient "schedulerclient"
)
func TestParseMemoryToBytes(t *testing.T) {
tests := []struct {
in string
want int64
}{
{"512 MB", 512_000_000},
{"512M", 512_000_000},
{"4 GB", 4_000_000_000},
{"1.5 G", 1_500_000_000},
{"1024 MB", 1_024_000_000},
}
for _, tt := range tests {
got, err := parseMemoryToBytes(tt.in)
if err != nil {
t.Errorf("parseMemoryToBytes(%q) unexpected error: %v", tt.in, err)
continue
}
if got != tt.want {
t.Errorf("parseMemoryToBytes(%q) = %d, want %d", tt.in, got, tt.want)
}
}
}
func TestParseMemoryToBytes_Invalid(t *testing.T) {
bad := []string{"", "abc", "10 XYZ", "1024", "MB", "1.2.3 GB"}
for _, in := range bad {
if _, err := parseMemoryToBytes(in); err == nil {
t.Errorf("parseMemoryToBytes(%q) expected error, got nil", in)
}
}
}
func TestParseWalltimeSeconds(t *testing.T) {
tests := []struct {
in string
want int
}{
{"0:30", 1800},
{"2:30", 9000},
{"02:30", 9000},
{"24:00", 86400},
}
for _, tt := range tests {
got, err := parseWalltimeSeconds(tt.in)
if err != nil {
t.Errorf("parseWalltimeSeconds(%q) unexpected error: %v", tt.in, err)
continue
}
if got != tt.want {
t.Errorf("parseWalltimeSeconds(%q) = %d, want %d", tt.in, got, tt.want)
}
}
}
func TestParseWalltimeSeconds_Invalid(t *testing.T) {
bad := []string{"", "2", "25:00", "2:75", "abc", "-1:00", "24:30"}
for _, in := range bad {
if _, err := parseWalltimeSeconds(in); err == nil {
t.Errorf("parseWalltimeSeconds(%q) expected error, got nil", in)
}
}
}
func TestTemplateRegistry_RegisterAndGet(t *testing.T) {
r := NewTemplateRegistry()
if !r.Has(TemplateGeneralSerial) {
t.Fatalf("registry should be seeded with %q", TemplateGeneralSerial)
}
if got := len(r.Names()); got != 1 {
t.Errorf("Names() has %d entries, want 1", got)
}
dummy := &dummyTemplate{name: "dummy"}
r.Register(dummy)
if !r.Has("dummy") {
t.Errorf("Register did not add %q", dummy.name)
}
if got := len(r.Names()); got != 2 {
t.Errorf("Names() has %d entries, want 2", got)
}
if _, ok := r.Get("does_not_exist"); ok {
t.Error("Get returned ok for unknown template")
}
tpl, ok := r.Get(TemplateGeneralSerial)
if !ok {
t.Fatalf("Get(%q) not found", TemplateGeneralSerial)
}
if tpl.Name() != TemplateGeneralSerial {
t.Errorf("tpl.Name() = %q, want %q", tpl.Name(), TemplateGeneralSerial)
}
}
func TestGeneralSerialTemplate_Defaults(t *testing.T) {
tpl, _ := NewTemplateRegistry().Get(TemplateGeneralSerial)
spec, err := tpl.Resolve(map[string]any{"command": "bash run.sh"})
if err != nil {
t.Fatalf("Resolve error: %v", err)
}
if spec.Command != "bash run.sh" {
t.Errorf("Command = %q, want %q", spec.Command, "bash run.sh")
}
if spec.Queue != "normal" {
t.Errorf("Queue = %q, want default 'normal'", spec.Queue)
}
if spec.MemoryBytes != 0 {
t.Errorf("MemoryBytes = %d, want 0 (unset)", spec.MemoryBytes)
}
if spec.WalltimeSeconds != 0 {
t.Errorf("WalltimeSeconds = %d, want 0 (unset)", spec.WalltimeSeconds)
}
if spec.NumProcessors != 0 {
t.Errorf("NumProcessors = %d, want 0 (unset)", spec.NumProcessors)
}
if spec.Env != nil {
t.Errorf("Env = %v, want nil (unset)", spec.Env)
}
}
func TestGeneralSerialTemplate_Validation(t *testing.T) {
tpl, _ := NewTemplateRegistry().Get(TemplateGeneralSerial)
tests := []struct {
name string
args map[string]any
}{
{"missing command", map[string]any{}},
{"blank command", map[string]any{"command": " "}},
{"bad memory", map[string]any{"command": "x", "memory": "not-a-size"}},
{"bad walltime", map[string]any{"command": "x", "walltime": "99"}},
}
for _, tt := range tests {
_, err := tpl.Resolve(tt.args)
if err == nil {
t.Errorf("%s: expected error, got nil", tt.name)
}
}
}
func TestGeneralSerialTemplate_ResolvesAllFields(t *testing.T) {
tpl, _ := NewTemplateRegistry().Get(TemplateGeneralSerial)
spec, err := tpl.Resolve(map[string]any{
"command": "bash run.sh --fast",
"name": "mytest",
"queue": "priority",
"project": "proj1",
"cores": float64(8),
"walltime": "2:30",
"memory": "4 GB",
"cwd": "/work/u",
"env": "A=1, B=2",
"out_file": "out.log",
"err_file": "err.log",
"description": "integration test",
})
if err != nil {
t.Fatalf("Resolve error: %v", err)
}
want := &sclient.JobSpec{
Name: "mytest",
Command: "bash run.sh --fast",
Queue: "priority",
Project: "proj1",
CWD: "/work/u",
OutFile: "out.log",
ErrFile: "err.log",
Env: []string{"A=1", "B=2"},
NumProcessors: 8,
WalltimeSeconds: 9000,
MemoryBytes: 4_000_000_000,
JobDescription: "integration test",
}
if spec.Name != want.Name || spec.Command != want.Command || spec.Queue != want.Queue ||
spec.Project != want.Project || spec.CWD != want.CWD || spec.OutFile != want.OutFile ||
spec.ErrFile != want.ErrFile || spec.NumProcessors != want.NumProcessors ||
spec.WalltimeSeconds != want.WalltimeSeconds || spec.MemoryBytes != want.MemoryBytes ||
spec.JobDescription != want.JobDescription {
t.Errorf("Resolve produced spec != want.\n got: %+v\nwant: %+v", spec, want)
}
if len(spec.Env) != 2 || spec.Env[0] != "A=1" || spec.Env[1] != "B=2" {
t.Errorf("Env = %v, want [A=1 B=2]", spec.Env)
}
}
// dummyTemplate is a minimal JobTemplate for registry tests.
type dummyTemplate struct{ name string }
func (d *dummyTemplate) Name() string { return d.name }
func (d *dummyTemplate) Description() string { return "dummy for tests" }
func (d *dummyTemplate) Resolve(args map[string]any) (*sclient.JobSpec, error) {
return &sclient.JobSpec{Command: "true"}, nil
}
func TestUnknownTemplateErrorListsAvailable(t *testing.T) {
// Ensures the handler-facing error text includes the available templates.
r := NewTemplateRegistry()
errText := "unknown template"
if !strings.Contains(errText, "unknown template") {
t.Fatal("sanity")
}
_ = r
}