package main import ( "os" "os/exec" "strings" "testing" "fn-registry/registry" ) // Signature with a bare "*" (PEP 3102) separating positional from keyword-only // params. This is the shape that used to make fn run emit "* = _args[3]". const kwOnlySig = "def add_event_dav(summary: str, start: str, end: str = '', *, location: str = '', all_day: bool = False) -> dict" func TestParsePySignatureBareStarKeywordOnly(t *testing.T) { params := parsePySignature(kwOnlySig) // The bare "*" marker must never surface as a real parameter. for _, p := range params { if p.Name == "*" { t.Fatalf("bare '*' leaked as a param: %+v", params) } } want := map[string]bool{ // name -> expected KwOnly "summary": false, "start": false, "end": false, "location": true, "all_day": true, } if len(params) != len(want) { t.Fatalf("got %d params, want %d: %+v", len(params), len(want), params) } for _, p := range params { kw, ok := want[p.Name] if !ok { t.Errorf("unexpected param %q", p.Name) continue } if p.KwOnly != kw { t.Errorf("param %q KwOnly=%v, want %v", p.Name, p.KwOnly, kw) } } } func TestGeneratePyRunnerKeywordOnlyValid(t *testing.T) { fn := ®istry.Function{ Name: "add_event_dav", Lang: "py", FilePath: "python/functions/pipelines/add_event_dav.py", Signature: kwOnlySig, } // All params are primitive, so no factory lookup happens and db is unused. script, err := generatePyRunner(fn, nil, "") if err != nil { t.Fatalf("generatePyRunner: %v", err) } if strings.Contains(script, "* = _args") { t.Fatalf("runner emitted invalid syntax '* = _args':\n%s", script) } // The signature default DEFAULT_BASE_URL (a module constant) must NOT be // replicated into the runner — that NameErrors at runtime. if strings.Contains(script, "DEFAULT_BASE_URL") { t.Errorf("runner replicated non-literal default DEFAULT_BASE_URL:\n%s", script) } // Required positionals are appended; keyword-only optionals go to kwargs. for _, want := range []string{ "_call_args.append(summary)", "_call_args.append(start)", `_call_kwargs["location"] = location`, `_call_kwargs["all_day"] = all_day`, "_result = add_event_dav(*_call_args, **_call_kwargs)", } { if !strings.Contains(script, want) { t.Errorf("missing %q in generated runner:\n%s", want, script) } } // The generated runner must itself be valid Python (compile, don't run). mustCompilePython(t, script) } // mustCompilePython checks the script parses as valid Python via py_compile. func mustCompilePython(t *testing.T, script string) { t.Helper() f, err := os.CreateTemp(t.TempDir(), "runner_*.py") if err != nil { t.Fatalf("temp file: %v", err) } if _, err := f.WriteString(script); err != nil { t.Fatalf("write: %v", err) } f.Close() py := pythonBinForTest() out, err := exec.Command(py, "-m", "py_compile", f.Name()).CombinedOutput() if err != nil { t.Fatalf("generated runner is not valid Python (%s): %v\n%s", py, err, out) } } // pythonBinForTest prefers the project venv, falling back to python3 on PATH. func pythonBinForTest() string { for _, c := range []string{"../../python/.venv/bin/python3", "python3"} { if c == "python3" { return c } if _, err := os.Stat(c); err == nil { return c } } return "python3" } // A "*args" var-positional marker must behave like the bare "*": skipped, and // everything after it treated as keyword-only. func TestParsePySignatureVarargsKeywordOnly(t *testing.T) { sig := "def f(a: str, *args, b: int = 0) -> dict" params := parsePySignature(sig) for _, p := range params { if strings.HasPrefix(p.Name, "*") { t.Fatalf("'*args' marker leaked as a param: %+v", params) } } if len(params) != 2 { t.Fatalf("got %d params, want 2: %+v", len(params), params) } got := map[string]bool{} for _, p := range params { got[p.Name] = p.KwOnly } if got["a"] != false || got["b"] != true { t.Errorf("KwOnly mismatch: a=%v (want false), b=%v (want true)", got["a"], got["b"]) } }