Auto E2E test run 2026-05-18 - #353
Merged
Merged
Conversation
The pinned `mcr.microsoft.com/dotnet/sdk:10.0@sha256:8a90a473...` image ships SDK 10.0.203, but global.json requires 10.0.300 (PR #346). Every `nuke DockerImageBuild` (and therefore the full compose stack and the E2E topology fixture) has been broken since #346 merged — there's no CI job that exercises DockerImageBuild, so the regression sat latent. Bumps: - dotnet/sdk:10.0 -> sha256:dc8430e6024d... (10.0.300) - dotnet/aspnet:10.0 -> sha256:9b5222b0ff8e... (10.0.300) - dotnet/sdk:10.0-alpine -> sha256:5c559aa5d993... (10.0.300) - dotnet/runtime-deps:10.0-alpine -> sha256:f276c0256ffc... qyl.mcp Dockerfile additionally pins the runtime stage to `--platform=linux/amd64`. Stage 1 cross-compiles with `-r linux-musl-x64`, but on arm64 hosts the runtime-deps multi-arch image resolves to arm64, producing an unbootable image (`ld-musl-x86_64.so.1 not found`). Pinning the runtime stage matches the cross-compiled binary's RID and is a no-op on amd64 hosts (where the runtime stage was implicitly amd64 anyway). Verified: `docker build -f services/qyl.{collector,mcp}/Dockerfile` both succeed; collector container returns 200 on /health; mcp container returns "Healthy" on /alive. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The bootstrap PR (#347) wired QylTopologyFixture but never executed it — only the WireMock seam test (Category=E2EBootstrap, no Docker) ran. Two latent defects in the fixture surface immediately on first real boot: 1. **Collector**: `WebApplication.CreateSlimBuilder(args)` defaults `ASPNETCORE_ENVIRONMENT` to `Production`, where `QYL_OTLP_AUTH_MODE` defaults to `ApiKey` (see `CollectorAuthExtensions.cs:22`). The collector then throws at startup because no `QYL_OTLP_PRIMARY_API_KEY` is set, and the /health wait strategy times out. Fix: set `QYL_OTLP_AUTH_MODE=Unsecured`. 2. **MCP**: The Dockerfile hard-codes `ENV ASPNETCORE_URLS=http://+:8080`, so the container listens on 8080 internally while the fixture binds 5200. The previous "Now listening on:" log-message wait succeeds (Kestrel logs it for port 8080), but `GetMappedPublicPort(5200)` then refers to a port nothing serves. Fix: override `ASPNETCORE_URLS` to 5200 to match the binding, and wait on the real `/alive` health endpoint instead of the log message. Also collapses the two-constructor pattern in QylTopologyFixture into a single one (xUnit v3 collection fixtures require exactly one public constructor — the dual ctors made the existing `ICollectionFixture<QylTopologyFixture>` registration throw `XunitException` at test discovery time). Verified by booting the topology end-to-end and POSTing to `/v1/traces` on the collector — the ingest path now returns 202 and the stored span is visible via `/api/v1/telemetry/stats`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…orage First real E2E scenario on top of the topology fixture from PR #347. Drives `POST /v1/traces` on the running qyl-collector container with a single-span OTLP/HTTP JSON payload (camelCase, hex-encoded ids, unique service.name per run for isolation), asserts the response is 202 Accepted, then polls `GET /api/v1/telemetry/stats` (bounded 15s, 250ms cadence) until `spanCount` increases past the pre-ingest baseline. Exercises the full pipeline inside one real container: HTTP receiver -> JSON parse -> OtlpConverter -> SpanRingBuffer.PushRange -> DuckDbStore.EnqueueAsync -> GetStorageStatsAsync. The richer roundtrip target (`GET /api/v1/traces`) is intentionally NOT asserted on because it currently returns HTTP 500 — the spans schema declares `kind VARCHAR NOT NULL` and `status_code VARCHAR NOT NULL` (DuckDbSchema.g.sql:316,319) but `SpanStorageRow.Kind` and `SpanStorageRow.StatusCode` are `byte`. The source-generated `MapFromReader` emits `reader.Col(N).GetByte(0)`, so every span read throws `InvalidCastException: Unable to cast object of type 'System.String' to type 'System.Byte'`. That is a pre-existing production bug to fix in a follow-up; the comment on the test class records the repro so the gap is discoverable. Stability: ran 4x locally, all pass in 4-7s per run. Class fixture shared across scenarios via [Collection(E2ECollection.Name)] so the topology is re-used by future test classes that join the collection. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Top-of-file entry summarizes the three commits on tests/auto-e2e-2026-05-18 (Dockerfile bumps, fixture testability fix, first real scenario), the verification results, and the production bug surfaced for follow-up: the spans VARCHAR-vs-byte read-mapping mismatch that breaks GET /api/v1/traces. Next-cycle handoff lists that bug fix as priority 1 ahead of additional scenarios. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This was referenced May 19, 2026
ANcpLua
added a commit
that referenced
this pull request
May 23, 2026
…ill builds The qyl.e2e.tests project was added in PR #353 and verified Debug-mode only. Three regressions accumulated between then and today blocked the Release build (which is what `nuke E2ETests` and `dotnet test -c Release` use): - Testcontainers deprecated the parameterless `ContainerBuilder()` ctor in favor of `ContainerBuilder("image-name")` (CS0618). The image is now passed positionally instead of via `.WithImage(...)`. - `DateTimeOffset.UtcNow` and `DateTime.UtcNow` in the OTLP scenario fire AL0026 / RS0030 in Release. The qyl convention is `TimeProvider.System.GetUtcNow()` everywhere outside low-level perf code. - `INetwork _network` was deleted via `DeleteAsync()` but never disposed, leaving the unmanaged handle (CA2213). `DisposeAsync()` performs the network delete and releases the handle. Also NoWarn `MultipleGlobalAnalyzerKeys` — same worktree-noise NoWarn the `Qyl.OpenTelemetry.SemanticConventions.SourceGeneration.Generator` csproj already carries: the parent `.globalconfig` and the worktree's identical copy are both ancestors of the project file, MSBuild unsets the duplicate keys and emits a per-key diagnostic. Harmless; not a real codebase issue. Verified: `dotnet build tests/qyl.e2e.tests -c Release` is 0 errors, 0 warnings. The existing OtlpHttpTraceIngestionRoundtripTests scenario still passes 3 consecutive runs against a freshly built collector+mcp topology. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ANcpLua
added a commit
that referenced
this pull request
May 23, 2026
Adds the second real E2E scenario on the qyl topology fixture and the
first one that exercises qyl-mcp directly. Until now the MCP container
was booted by the fixture but no scenario hit it — its only assurance
was the `/alive` health probe, which proves nothing about the source-
generator-emitted tool catalog or skill bundle loading.
## Scenario
- Actor: an AI-agent connector (Anthropic, OpenAI, custom) or an
operator validating a fresh deploy.
- Flow: GET /llms.txt against the running qyl-mcp container.
- Expected outcome: 200 OK, text/plain, body advertises the qyl heading,
the server summary, the documented Streamable HTTP transport, a tool
count >= 1, a capability count >= 1, the documented discovery tools
(`qyl.list_capabilities`, `qyl.get_capability_guide`), and the
enabled-capabilities section header.
The `[1-9][0-9]*` regex on tool/capability counts catches the failure
mode source generators most often produce — a zero-count catalog from a
trimmer regression or a skipped `nuke Generate` step would silently pass
any `Contains("Tool count:")` substring check.
One scenario per class, one [Fact]. Joins `E2ECollection` so the shared
fixture is reused; topology setup remains ~4 s + ~1 s per scenario.
## Stability
3 consecutive Release-mode runs (`dotnet test ... -c Release`), all
green: 7.5 s cold + 5.0 s + 5.0 s. Zero flakes. Mutation: not applicable
at the E2E level.
## Gaps remaining for next routine run
1. Spans VARCHAR-vs-byte read-mapping bug (carry-forward from PR #353)
still blocks `GET /api/v1/traces`. Higher-priority data-correctness
defect, but outside the E2E routine's scope — should land via the
integration-tests routine first, then this suite can extend to assert
the full ingest+read roundtrip.
2. MCP JSON-RPC scenario: drive `initialize` + `tools/list` over the
Streamable HTTP `/mcp` endpoint, not just the static `/llms.txt`
projection. Will exercise the actual MCP transport machinery.
3. qyl-loom is in `eng/compose.yaml` and built by the Nuke
`DockerImageBuild` target but absent from the topology fixture.
Adding it requires an OPENAI_API_KEY stub via the existing WireMock
LLM seam.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
First real E2E scenario landed on top of PR #347's bootstrap topology, plus
two pre-existing infrastructure bugs unblocked along the way. POST OTLP/HTTP
JSON to the running qyl-collector container, assert the ingest lands in
DuckDB and is reflected in
/api/v1/telemetry/stats. Topology fixtureboots both qyl-collector and qyl-mcp end-to-end for the first time since
#347 wired it up.
Scenario
to qyl-collector (
/v1/traces)endpoint (
/api/v1/telemetry/stats) reflects the span (spanCountincreases, newestSpanTime >= the ingest timestamp)
One scenario per class, one
[Fact]per class. JoinsE2ECollectionvia[Collection(E2ECollection.Name)]so the fixture is shared with futurescenarios.
Topology booted
qyl-collector:latest(locally built fromservices/qyl.collector/Dockerfile)qyl-mcp:latest(locally built fromservices/qyl.mcp/Dockerfile)but not exercised by this scenario)
ICompositeContainer-style — three containerson a fresh
qyl-e2e-*Docker network per fixture lifetime). TUnit.Aspirenot yet adopted in qyl.
Assertions
202 Accepted/api/v1/telemetry/statsspanCountincreases past the pre-ingest baselinewithin 15 s (bounded poll, 250 ms cadence)
/api/v1/telemetry/statsnewestSpanTime>= the test's start nano timestampRefactors for testability (separate commits)
c2a4fd18refactor(tests/e2e): make topology fixture actually boot collector + mcp— three fixes that the bootstrap PR (deps: bump MCP 1.2 -> 1.3 + e2e bootstrap #347) couldn'tsurface because the fixture itself was never executed:
QYL_OTLP_AUTH_MODE=Unsecuredto collector (default isApiKeyin Production and requires API key configuration; collector aborted
on startup before this).
ASPNETCORE_URLS=http://+:5200to MCP (the Dockerfile hard-codes8080, so the previous
WithPortBinding(5200, true)mapped to a portnothing served).
/alivehealth endpoint instead of"Now listening on:"log scrape.QylTopologyFixtureto a singlepublic ctor (xUnit v3
ICollectionFixture<T>requires exactly one).Production fixes shipped (separate commit)
bede0b1ffix(docker): bump .NET base images so SDK 10.0.300 actually builds— the pinnedmcr.microsoft.com/dotnet/sdk:10.0@sha256:8a90a473...shipped SDK 10.0.203, but
global.jsonrequires 10.0.300 since PR deps: bump .NET SDK to 10.0.300 + ANcpLua MSBuild SDKs #346.Every
nuke DockerImageBuild(and therefore the full compose stack andthe E2E topology) was broken since deps: bump .NET SDK to 10.0.300 + ANcpLua MSBuild SDKs #346 merged — no CI job exercises
DockerImageBuild, so the regression sat latent. Bumped sdk:10.0,
aspnet:10.0, sdk:10.0-alpine, and runtime-deps:10.0-alpine to the
current latest SHAs.
qyl.mcpruntime stage to--platform=linux/amd64to match its
-r linux-musl-x64cross-compile target. arm64 dev hosts(Apple Silicon + OrbStack/Docker Desktop) previously couldn't run the
image because runtime-deps resolved to arm64, producing
ld-musl-x86_64.so.1 not found. No-op on amd64 hosts.Production bug surfaced (NOT fixed here — separate concern)
GET /api/v1/tracesis fully broken: schema declaresspans.kind VARCHAR NOT NULLandspans.status_code VARCHAR NOT NULL(
services/qyl.collector/Storage/DuckDbSchema.g.sql:316,319), butSpanStorageRow.KindandSpanStorageRow.StatusCodearebyte. Thesource-generated
MapFromReader(
internal/qyl.collector.storage.generators/DuckDbEmitter.cs:219-222)emits
reader.Col(N).GetByte(0), so every span read throwsInvalidCastException: Unable to cast object of type 'System.String' to type 'System.Byte'and the endpoint returns 500.Repro (against a fresh collector container with
QYL_OTLP_AUTH_MODE=Unsecured):Fix is either schema migration to
TINYINT(and similarly forspans.kind) or property type change tostring. NOT fixed in this PRbecause (a) it's unrelated to E2E coverage, (b) a schema migration needs
its own focused PR with migration-up/down testing and the integration-test
suite extended, and (c) it's the higher-priority next-cycle item — the
routine handoff records this as gap #1.
This scenario asserts via
/api/v1/telemetry/statsinstead (which works— it doesn't go through
SpanStorageRow.MapFromReader), so theend-to-end ingest pipeline is verified without depending on the broken
read path.
xUnit -> TUnit migrations
None. Repo standard is xUnit.v3 with
xunit.v3.mtp-v2(verified acrossqyl.collector.tests,qyl.collector.integration.tests,qyl.mcp.tests,and the existing
qyl.e2e.testsbootstrap). New scenario matches theexisting style.
Runtime
/health, MCP ~2 s on/alive)Stability check
4 consecutive local runs, all passed:
Zero flakes.
Mutation: not applicable at the E2E level.
Gaps remaining for next run
real production data-fetch defect, not an E2E concern). Once fixed,
extend this scenario to also assert via
GET /api/v1/traces/{traceId}for a true read+write roundtrip.
now boots; drive an MCP tool that reads collector data (POST OTLP
first, then invoke an MCP search tool over JSON-RPC, assert the span
is returned).
redacted — the original handoff candidate. Larger surface (needs a
sink container plus LLM wiring); do after Add Claude Code GitHub Workflow #1 and refactor: internal OTLP types + remove ghost code #2.
🤖 Generated with Claude Code