test: integration suite with real Traefik, redis-shared limits, streaming assertions

Adds integration/ (separate module): docker compose stack with real Traefik
v3.5 + localPlugins, mock openai/anthropic/gemini/tool providers, shared
Redis, and a raw-TCP streaming timing probe. 6/7 tests pass; streaming
incrementality fails on a confirmed external yaegi/Traefik bug (see
task-15-report.md), not a plugin defect.

Fixes 4 real Yaegi-interpretation bugs surfaced by running the plugin under
real Traefik for the first time (none caught by go test or yaegi-check,
which never exercises chat/embeddings/streaming/Redis):

- .traefik.yml: add basePkg (Traefik derives the yaegi binding identifier
  from the module path, not the package's own declared name; without this
  override the plugin never loads at all).
- registry.go: resolveAgainst's multi-value return panicked yaegi on an
  inline string concatenation; assign to a local first.
- routes_unified.go: errors.As panicked yaegi on interpreted pointer error
  types; use plain type assertions (both types are always returned bare,
  never wrapped).
- sse.go: readSSE's dispatch closure hit the same multi-value-assignment
  panic as registry.go; split into separate statements.
- resp.go: respErr as a defined string type was indistinguishable from a
  plain string in a yaegi type assertion, misreading every successful
  Redis SELECT reply as failed and silently disabling rate limiting;
  changed to a struct.
This commit is contained in:
2026-08-20 13:28:20 +01:00
parent 304c5ebf3f
commit 7939c3e74b
14 changed files with 1149 additions and 12 deletions
+12 -1
View File
@@ -368,7 +368,18 @@ func (m *modelRegistry) resolveAgainst(providerName, upstreamModel, requestedID
if !allowed || !grp.allowsProvider(providerName) {
return nil, "", "", errModelDenied
}
return m.adapters[providerName], upstreamModel, providerName + "/" + upstreamModel, nil
// canonical is assigned to a local before the return, not inlined into
// it: yaegi v0.16.1 panics ("reflect.Set: value of type
// interp.valueInterface is not assignable to type string") building a
// multi-value return tuple when one element is an inline string
// concatenation of two parameters. The identical expression assigned
// to a variable first, then returned, does not trigger it. Verified
// empirically against yaegi v0.16.1 running this exact function under
// real Traefik (Task 15's integration suite) — tools/yaegi-check never
// exercises this call path (it only drives GET /v1/models), which is
// why the existing yaegi gate never caught it.
canonical := providerName + "/" + upstreamModel
return m.adapters[providerName], upstreamModel, canonical, nil
}
// warnCollisionOnce logs, at most once per colliding bare id for this