chore: avance acumulado de sesiones previas (reorg dev/issues + ajustes)
Reorganizacion de dev/issues en subcarpetas (completed/, cpp/, gamedev/, kanban/, trading/, imagegen/, matrix/) y cambios acumulados en cmd/fn/pyrunner, .claude/commands y settings. Trabajo de otro LLM/sesion, commiteado a peticion del usuario para desbloquear el working tree. Excluido logs/ardour_mcp_server.log (ruido).
This commit is contained in:
@@ -0,0 +1,141 @@
|
||||
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"])
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user