refactor(dynamic): extend framework-specific fallbacks with Spring HandlerExecutionChain, Go gqlgen, Django handler/middleware chain, Celery task registry, and Sidekiq client handling; enhance coverage and test logic

This commit is contained in:
elipeter 2026-05-27 15:29:52 -05:00
parent fd39304eed
commit 71fade1d83
5 changed files with 356 additions and 24 deletions

View file

@ -140,6 +140,18 @@ fn go_string_literal(s: &str) -> String {
format!("\"{escaped}\"")
}
fn go_identifier_expr(name: &str) -> Option<String> {
let mut chars = name.chars();
let first = chars.next()?;
if !(first == '_' || first.is_ascii_alphabetic()) {
return None;
}
if !chars.all(|c| c == '_' || c.is_ascii_alphanumeric()) {
return None;
}
Some(format!("entry.{name}"))
}
/// Sorted, deduped tab-prefixed import lines covering the driver's
/// `fmt` + `os` plus everything in [`SHIM_IMPORTS`].
fn chain_step_imports() -> String {
@ -2613,6 +2625,96 @@ fn emit_graphql_resolver_harness(
) -> HarnessSource {
let shim = probe_shim();
let go_mod = generate_go_mod_for_spec(GoShape::Generic, spec);
let handler_expr = go_identifier_expr(handler).unwrap_or_else(|| "nil".to_owned());
let use_gqlgen_runtime = spec
.framework
.as_ref()
.map(|binding| binding.adapter == "graphql-gqlgen")
.unwrap_or(false);
let runtime_imports = if use_gqlgen_runtime {
r#" "bytes"
"encoding/json"
"net/http/httptest"
"github.com/99designs/gqlgen/graphql"
gqlhandler "github.com/99designs/gqlgen/graphql/handler"
"github.com/vektah/gqlparser/v2"
"github.com/vektah/gqlparser/v2/ast"
"github.com/vektah/gqlparser/v2/gqlerror"
"#
} else {
""
};
let runtime_call = if use_gqlgen_runtime {
"\tif nyxTryGqlgenHandler(cb, payload) {\n\t\treturn\n\t}\n"
} else {
""
};
let runtime_helpers = if use_gqlgen_runtime {
format!(
r##"
type nyxExecutableSchema struct {{
schema *ast.Schema
resolver reflect.Value
payload string
field string
}}
func (s *nyxExecutableSchema) Schema() *ast.Schema {{
return s.schema
}}
func (s *nyxExecutableSchema) Complexity(typeName, fieldName string, childComplexity int, args map[string]interface{{}}) (int, bool) {{
return 1, true
}}
func (s *nyxExecutableSchema) Exec(ctx context.Context) graphql.ResponseHandler {{
return func(ctx context.Context) *graphql.Response {{
value, err := nyxInvokeResolverValue(s.resolver, s.payload)
if err != nil {{
return &graphql.Response{{Errors: gqlerror.List{{gqlerror.Errorf(err.Error())}}}}
}}
data, err := json.Marshal(map[string]interface{{}}{{s.field: fmt.Sprint(value)}})
if err != nil {{
return &graphql.Response{{Errors: gqlerror.List{{gqlerror.Errorf(err.Error())}}}}
}}
return &graphql.Response{{Data: json.RawMessage(data)}}
}}
}}
func nyxTryGqlgenHandler(cb reflect.Value, payload string) bool {{
schema, err := gqlparser.LoadSchema(&ast.Source{{
Name: "nyx.graphql",
Input: "schema {{ query: Query }}\ntype Query {{ {field}(id: String, input: String): String }}",
}})
if err != nil {{
fmt.Fprintf(os.Stderr, "NYX_GQLGEN_SCHEMA_FALLBACK: %v\n", err)
return false
}}
server := gqlhandler.NewDefaultServer(&nyxExecutableSchema{{
schema: schema, resolver: cb, payload: payload, field: "{field}",
}})
body, _ := json.Marshal(map[string]interface{{}}{{
"query": "query($value: String) {{ {field}(id: $value, input: $value) }}",
"variables": map[string]interface{{}}{{"value": payload}},
}})
req := httptest.NewRequest("POST", "/query", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
rec := httptest.NewRecorder()
server.ServeHTTP(rec, req)
if rec.Code < 200 || rec.Code >= 300 {{
fmt.Fprintf(os.Stderr, "NYX_GQLGEN_HANDLER_FALLBACK: status=%d body=%s\n", rec.Code, rec.Body.String())
return false
}}
fmt.Print(rec.Body.String())
return true
}}
"##,
field = field
)
} else {
String::new()
};
let source = format!(
r##"// Nyx dynamic harness — GraphQL resolver (Phase 21 / Track M.3).
package main
@ -2622,6 +2724,7 @@ import (
"fmt"
"os"
"reflect"
{runtime_imports}
"nyx-harness/entry"
)
@ -2640,37 +2743,67 @@ func main() {{
payload := nyxPayload()
fmt.Println("__NYX_GRAPHQL_RESOLVER__: " + "{type_name}" + "." + "{field}")
fmt.Println("__NYX_SINK_HIT__")
cb, ok := entry.NyxResolvers["{handler}"]
if !ok {{
cb := reflect.ValueOf({handler_expr})
if !cb.IsValid() || cb.Kind() != reflect.Func {{
fmt.Fprintln(os.Stderr, "NYX_RESOLVER_NOT_FOUND: " + "{handler}")
os.Exit(78)
}}
v := reflect.ValueOf(cb)
args := make([]reflect.Value, v.Type().NumIn())
for i := 0; i < v.Type().NumIn(); i++ {{
want := v.Type().In(i)
if want.Kind() == reflect.String {{
args[i] = reflect.ValueOf(payload)
}} else if want.String() == "context.Context" {{
args[i] = reflect.ValueOf(context.Background())
}} else {{
args[i] = reflect.Zero(want)
}}
}}
{runtime_call}
defer func() {{
if r := recover(); r != nil {{
fmt.Fprintf(os.Stderr, "NYX_EXCEPTION: panic: %v\n", r)
}}
}}()
out := v.Call(args)
if len(out) > 0 {{
fmt.Println(out[0].Interface())
value, err := nyxInvokeResolverValue(cb, payload)
if err != nil {{
fmt.Fprintf(os.Stderr, "NYX_EXCEPTION: %v\n", err)
return
}}
if value != nil {{
fmt.Println(value)
}}
}}
func nyxInvokeResolverValue(v reflect.Value, payload string) (interface{{}}, error) {{
contextType := reflect.TypeOf((*context.Context)(nil)).Elem()
errorType := reflect.TypeOf((*error)(nil)).Elem()
args := make([]reflect.Value, v.Type().NumIn())
for i := 0; i < v.Type().NumIn(); i++ {{
want := v.Type().In(i)
if want.Kind() == reflect.String {{
args[i] = reflect.ValueOf(payload)
}} else if want.Implements(contextType) {{
args[i] = reflect.ValueOf(context.Background())
}} else if contextType.AssignableTo(want) {{
args[i] = reflect.ValueOf(context.Background())
}} else {{
args[i] = reflect.Zero(want)
}}
}}
out := v.Call(args)
var value interface{{}}
for _, item := range out {{
if item.Type().Implements(errorType) {{
if (item.Kind() == reflect.Interface || item.Kind() == reflect.Pointer) && !item.IsNil() {{
return nil, item.Interface().(error)
}}
continue
}}
if value == nil && item.IsValid() {{
value = item.Interface()
}}
}}
return value, nil
}}
{runtime_helpers}
"##,
handler = handler,
handler_expr = handler_expr,
type_name = type_name,
field = field,
runtime_imports = runtime_imports,
runtime_call = runtime_call,
runtime_helpers = runtime_helpers,
);
HarnessSource {
source,

View file

@ -4687,9 +4687,6 @@ public class NyxHarness {{
System.exit(78);
}}
m.setAccessible(true);
if (nyxTrySpringHandlerInterceptor(instance, m, payload)) {{
return;
}}
Class<?>[] params = m.getParameterTypes();
Object[] mArgs = new Object[params.length];
for (int i = 0; i < params.length; i++) {{
@ -4810,6 +4807,9 @@ public class NyxHarness {{
System.exit(78);
}}
m.setAccessible(true);
if (nyxTrySpringHandlerExecutionChain(instance, m, payload)) {{
return;
}}
Class<?>[] params = m.getParameterTypes();
Object[] mArgs = new Object[params.length];
for (int i = 0; i < params.length; i++) {{
@ -4835,6 +4835,57 @@ public class NyxHarness {{
return "";
}}
static boolean nyxTrySpringHandlerExecutionChain(Object instance, Method m, String payload) {{
if (!m.getName().equals("preHandle") || m.getParameterTypes().length < 3) {{
return false;
}}
try {{
Class<?> chainClass = Class.forName("org.springframework.web.servlet.HandlerExecutionChain");
Class<?> interceptorClass = Class.forName("org.springframework.web.servlet.HandlerInterceptor");
if (!interceptorClass.isAssignableFrom(instance.getClass())) {{
return false;
}}
Object interceptors = java.lang.reflect.Array.newInstance(interceptorClass, 1);
java.lang.reflect.Array.set(interceptors, 0, instance);
Object chain = chainClass
.getConstructor(Object.class, interceptors.getClass())
.newInstance(new Object(), interceptors);
Method getInterceptors = chainClass.getMethod("getInterceptors");
Object chainInterceptors = getInterceptors.invoke(chain);
int count = chainInterceptors == null ? 0 : java.lang.reflect.Array.getLength(chainInterceptors);
if (count == 0) {{
return false;
}}
Object request = null;
Object response = null;
for (Class<?> p : m.getParameterTypes()) {{
String name = p.getName();
if (request == null && name.endsWith("HttpServletRequest")) {{
request = nyxServletProxy(p, payload);
}} else if (response == null && name.endsWith("HttpServletResponse")) {{
response = nyxServletProxy(p, payload);
}}
}}
if (request == null || response == null) {{
return false;
}}
Object interceptor = java.lang.reflect.Array.get(chainInterceptors, 0);
Method preHandle = interceptor.getClass().getMethod(
"preHandle",
m.getParameterTypes()[0],
m.getParameterTypes()[1],
m.getParameterTypes()[2]
);
preHandle.invoke(interceptor, request, response, new Object());
return true;
}} catch (ClassNotFoundException missingSpring) {{
return false;
}} catch (Throwable e) {{
System.err.println("NYX_SPRING_CHAIN_FALLBACK: " + e.getClass().getName() + ": " + e.getMessage());
return false;
}}
}}
static boolean nyxTrySpringHandlerInterceptor(Object instance, Method m, String payload) {{
Class<?>[] params = m.getParameterTypes();
if (params.length < 3 || !m.getName().equals("preHandle")) {{

View file

@ -1536,8 +1536,64 @@ def _nyx_try_celery_eager(task, body):
print(f"NYX_CELERY_EAGER_FALLBACK: {{type(_e).__name__}}: {{_e}}", file=sys.stderr, flush=True)
return False
def _nyx_try_celery_registered_task(handler_name, task, body):
try:
from celery import current_app
except Exception:
return False
try:
app = getattr(task, "app", None) or current_app
if app is None or not hasattr(app, "tasks"):
return False
if hasattr(app, "conf"):
try:
app.conf.task_always_eager = True
app.conf.task_eager_propagates = False
except Exception:
pass
candidates = [
handler_name,
getattr(task, "name", None),
getattr(_entry_mod, "__name__", "") + "." + handler_name,
]
registered = None
for name in candidates:
if not name:
continue
try:
registered = app.tasks.get(name)
except Exception:
registered = None
if registered is not None:
break
if registered is None:
suffix = "." + handler_name
try:
for task_name, candidate in app.tasks.items():
if str(task_name).endswith(suffix):
registered = candidate
break
except Exception:
registered = None
if registered is None:
return False
if hasattr(registered, "signature"):
sig = registered.signature(args=(body,))
result = sig.apply(throw=False)
else:
result = registered.apply(args=(body,), throw=False)
value = getattr(result, "result", None)
if value is not None:
print(str(value), flush=True)
return True
except SystemExit:
raise
except Exception as _e:
print(f"NYX_CELERY_REGISTRY_FALLBACK: {{type(_e).__name__}}: {{_e}}", file=sys.stderr, flush=True)
return False
try:
if not _nyx_try_celery_eager(_h, payload):
if not _nyx_try_celery_registered_task({handler:?}, _h, payload) and not _nyx_try_celery_eager(_h, payload):
_result = _h(payload)
if _result is not None:
try:
@ -1842,7 +1898,54 @@ try:
print(f"NYX_DJANGO_MIDDLEWARE_FALLBACK: {{type(_e).__name__}}: {{_e}}", file=sys.stderr, flush=True)
return False
if not _nyx_try_django_middleware(_h, payload):
def _nyx_try_django_handler_chain(factory, body):
try:
import types
from django.conf import settings
module_name = "_nyx_phase21_middleware"
module = types.ModuleType(module_name)
setattr(module, "NyxMiddleware", factory)
module.urlpatterns = []
sys.modules[module_name] = module
middleware_path = module_name + ".NyxMiddleware"
if not settings.configured:
settings.configure(
DEFAULT_CHARSET="utf-8",
SECRET_KEY="nyx-dynamic-harness",
ROOT_URLCONF=module_name,
ALLOWED_HOSTS=["testserver", "localhost", "127.0.0.1"],
INSTALLED_APPS=[],
MIDDLEWARE=[middleware_path],
)
else:
try:
settings.MIDDLEWARE = [middleware_path]
settings.ROOT_URLCONF = module_name
except Exception:
return False
import django
django.setup()
from django.core.handlers.base import BaseHandler
from django.test import RequestFactory
request = RequestFactory().post("/nyx", data={{"q": body}})
request._body = str(body).encode("utf-8", "replace")
handler = BaseHandler()
handler.load_middleware()
response = handler.get_response(request)
body_bytes = getattr(response, "content", None)
if body_bytes:
try:
print(body_bytes.decode("utf-8", "replace"), flush=True)
except Exception:
print(str(body_bytes), flush=True)
return True
except SystemExit:
raise
except Exception as _e:
print(f"NYX_DJANGO_HANDLER_CHAIN_FALLBACK: {{type(_e).__name__}}: {{_e}}", file=sys.stderr, flush=True)
return False
if not _nyx_try_django_handler_chain(_h, payload) and not _nyx_try_django_middleware(_h, payload):
_req = _NyxRequest(payload)
# Try class-shaped middleware (instantiate with a get_response stub).
try:

View file

@ -765,7 +765,12 @@ if Object.const_defined?({handler:?})
require 'sidekiq/testing'
if cls.respond_to?(:perform_async)
Sidekiq::Testing.inline! do
cls.perform_async($nyx_payload)
begin
require 'sidekiq/client'
Sidekiq::Client.push('class' => cls, 'args' => [$nyx_payload])
rescue LoadError, StandardError
cls.perform_async($nyx_payload)
end
end
exit 0
end

View file

@ -680,6 +680,9 @@ fn scheduled_job_python_harness_carries_sentinel_and_handler() {
assert!(h.source.contains("__NYX_SCHEDULED_JOB__"));
assert!(h.source.contains("\"tick\""));
assert!(h.source.contains("*/5 * * * *"));
assert!(h.source.contains("_nyx_try_celery_registered_task"));
assert!(h.source.contains("current_app"));
assert!(h.source.contains("app.tasks"));
assert!(h.source.contains("_nyx_try_celery_eager"));
assert!(h.source.contains("task.apply"));
}
@ -714,6 +717,10 @@ fn scheduled_job_java_harness_carries_sentinel_and_handler() {
assert!(h.source.contains("\"execute\""));
assert!(h.source.contains("nyxTryQuartz"));
assert!(h.source.contains("org.quartz.JobBuilder"));
assert!(
!h.source
.contains("nyxTrySpringHandlerInterceptor(instance, m, payload)")
);
assert_eq!(h.command, vec!["java", "-cp", ".:lib/*", "NyxHarness"]);
}
@ -729,6 +736,7 @@ fn scheduled_job_ruby_harness_carries_sentinel_and_handler() {
assert!(h.source.contains("__NYX_SCHEDULED_JOB__"));
assert!(h.source.contains("TickWorker"));
assert!(h.source.contains("sidekiq/testing"));
assert!(h.source.contains("Sidekiq::Client.push"));
assert!(h.source.contains("perform_async"));
}
@ -873,7 +881,34 @@ fn graphql_resolver_go_harness_carries_sentinel_and_field() {
let h = lang::emit(&spec).expect("emit ok");
assert!(h.source.contains("__NYX_GRAPHQL_RESOLVER__"));
assert!(h.source.contains("ResolveUser"));
assert!(h.source.contains("entry.NyxResolvers"));
assert!(h.source.contains("reflect.ValueOf(entry.ResolveUser)"));
assert!(!h.source.contains("entry.NyxResolvers"));
}
#[test]
fn graphql_resolver_go_gqlgen_harness_uses_handler_runtime() {
let spec = framework_bound_spec(
Lang::Go,
EvEntryKind::GraphQLResolver {
type_name: "Query".into(),
field: "user".into(),
},
"ResolveUser",
"tests/dynamic_fixtures/graphql_resolver/gqlgen/vuln.go",
"graphql-gqlgen",
);
let h = lang::emit(&spec).expect("emit ok");
assert!(
h.source
.contains("github.com/99designs/gqlgen/graphql/handler")
);
assert!(h.source.contains("gqlhandler.NewDefaultServer"));
assert!(h.source.contains("httptest.NewRecorder"));
assert!(h.source.contains("nyxExecutableSchema"));
assert!(h.source.contains(
"Complexity(typeName, fieldName string, childComplexity int, args map[string]interface{})"
));
assert!(!h.source.contains("entry.NyxResolvers"));
}
#[test]
@ -945,6 +980,9 @@ fn middleware_python_harness_carries_sentinel_and_handler() {
let h = lang::emit(&spec).expect("emit ok");
assert!(h.source.contains("__NYX_MIDDLEWARE__"));
assert!(h.source.contains("\"audit\""));
assert!(h.source.contains("_nyx_try_django_handler_chain"));
assert!(h.source.contains("BaseHandler"));
assert!(h.source.contains("handler.load_middleware"));
assert!(h.source.contains("_nyx_try_django_middleware"));
assert!(h.source.contains("RequestFactory"));
}
@ -979,6 +1017,8 @@ fn middleware_java_harness_carries_sentinel_and_handler() {
let h = lang::emit(&spec).expect("emit ok");
assert!(h.source.contains("__NYX_MIDDLEWARE__"));
assert!(h.source.contains("\"preHandle\""));
assert!(h.source.contains("nyxTrySpringHandlerExecutionChain"));
assert!(h.source.contains("HandlerExecutionChain"));
assert!(h.source.contains("nyxTrySpringHandlerInterceptor"));
assert!(h.source.contains("HttpServletRequest"));
}