Skip to content

feat(http): add RespondBytes for raw binary response payloads - #133

Merged
ncipollina merged 5 commits into
mainfrom
feat/http-respond-bytes
Sep 4, 2026
Merged

feat(http): add RespondBytes for raw binary response payloads#133
ncipollina merged 5 commits into
mainfrom
feat/http-respond-bytes

Conversation

@ncipollina

Copy link
Copy Markdown
Contributor

Summary

Adds HttpResponseRegistrationBuilder.RespondBytes(byte[], mediaType) to Compono.Http, closing a gap between RespondText and RespondJson: no way to serve a raw binary payload. Surfaced by dogfood evidence in alexa-vox-craft, where a test needing to serve a fetched certificate's DER bytes had to round-trip them through Encoding.Latin1 text as a workaround.

Changes

  • src/Compono.Http/HttpResponseRegistrationBuilder.cs - new RespondBytes(byte[] content, string mediaType = "application/octet-stream"), following RespondJson's serialize-once-to-bytes model (content captured once, a fresh ByteArrayContent + MediaTypeHeaderValue per matched invocation).
  • docs/adr/0051-compono-http-handler-based-testing-package.md - recorded as Amendment 2 (purely additive, source-compatible extension, not a reversal of the original decision).
  • test/Compono.Http.Tests/TestHttpHandlerTests.cs - two new tests: byte round-trip with default content type, and custom media type.
  • docs/reference/api/Compono.Http/ - regenerated via .github/scripts/generate-api-reference.sh.

Validation

  • dotnet test test/Compono.Http.Tests/Compono.Http.Tests.csproj -f net10.0 - 31/31 passed
  • Also ran the two new tests standalone on net9.0
  • API reference regenerated from a Release/net10.0 build; diff scoped to the new method's page + index entry

Closes the gap between RespondText and RespondJson - a consumer needing
to serve raw bytes (e.g. a fetched certificate's DER bytes) previously
had to round-trip through a Latin1-encoded string, an awkward workaround
surfaced by dogfood evidence in alexa-vox-craft. Follows RespondJson's
serialize-once-to-bytes model; recorded as ADR-0051 Amendment 2.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GMQEjFrUVGYkctE3ECfRuA
@github-actions github-actions Bot added the type: feat New feature label Sep 4, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f035438b42

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/Compono.Http/HttpResponseRegistrationBuilder.cs Outdated
Comment thread src/Compono.Http/HttpResponseRegistrationBuilder.cs
j-d-ha
j-d-ha previously approved these changes Sep 4, 2026

@j-d-ha j-d-ha left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

…ge guide

Address Codex review feedback on PR #133: RespondBytes retained the
caller's byte[] by reference instead of copying it, so a post-registration
mutation or buffer reuse would silently change an already-registered
response - breaking the serialize-once snapshot semantics RespondJson
already provides and this method's own doc claimed to match. Also add
RespondBytes to docs/packages/compono-http.md's Response APIs list, which
was updated everywhere except the consumer-facing package guide.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GMQEjFrUVGYkctE3ECfRuA
@ncipollina

Copy link
Copy Markdown
Contributor Author

@codex review commit 4bc5838 — addresses both prior findings: RespondBytes now defensively copies content at registration (HttpResponseRegistrationBuilder.cs), and RespondBytes was added to docs/packages/compono-http.md's Response APIs list.

…edback

The task file's Phase 3 covered replying to and resolving feedback but
never mentioned requesting a fresh bot review scoped to the fix commit
once real code changes are pushed - an established pattern this session
used, now made explicit so it isn't re-derived ad hoc next time.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GMQEjFrUVGYkctE3ECfRuA

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4bc5838f5a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/Compono.Http/HttpResponseRegistrationBuilder.cs
Comment thread src/Compono.Http/HttpResponseRegistrationBuilder.cs
ncipollina and others added 2 commits September 4, 2026 15:11
…erence

The published compono skill's http.md enumerated every Respond* finalizer
for consumers writing Compono.Http tests, but was written before
RespondBytes existed (PR #133). Add it alongside the others so skill
guidance doesn't quietly fall behind the package's actual API surface.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GMQEjFrUVGYkctE3ECfRuA
Address Codex review feedback on PR #133: PLAN-0051's Response APIs
(Task 4) and Behavioral tests (Task 9) checklists still only enumerated
the pre-Amendment-2 Respond* set, so the plan no longer accurately
reflected Compono.Http's completed public behavior and test coverage.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GMQEjFrUVGYkctE3ECfRuA
@ncipollina
ncipollina merged commit 87284e4 into main Sep 4, 2026
11 checks passed
@ncipollina
ncipollina deleted the feat/http-respond-bytes branch September 4, 2026 19:19
ncipollina added a commit that referenced this pull request Sep 7, 2026
…kage-validation CI, regen API docs

Three real generator-correctness bugs found by automated PR review, each with a
new generator-fixture regression test:

- ClearCalls()/ReceivedCalls() are always-emitted, always-zero-argument bridge
  extensions exactly like Configure()/Verify(), but the CMP0023 reserved-name
  collision check only covered the latter two - an interface declaring its own
  zero-argument ClearCalls/ReceivedCalls member silently shadowed the generated
  bridge with no diagnostic. Widened the reserved-name set in TestDoubleAnalyzer.
- An eligible member's generated {FieldName}_ReceivedCall record-class name could
  collide with an unrelated sibling member's own natural field name, producing a
  real CS0102 duplicate-declaration compile error. Fixed by feeding this derived
  name into the existing derivedAuxiliaryNameOwners collision pre-pass, which
  demotes the colliding member out of matching eligibility instead.
- A parameter literally named the same as its own member's generated record type
  produced a positional record property sharing the enclosing type's name
  (CS0542). Fixed in TestDouble.scriban by suffixing that one parameter's
  declared name inside the record declaration only.

Also fixes two unrelated CI blockers surfaced on this PR:
- .github/workflows/package-validation.yaml never set -p:Version on its
  validation-only pack, defaulting to 1.0.0.0 - this started failing ApiCompat's
  CP0003 the moment nuget.org's published baseline crossed 1.0.0
  (1.1.0-preview.103, published by PR #133's merge to main). Every PR's
  package-validation would fail this way regardless of content; pinned the
  packed Version to the resolved baseline.
- docs/reference/api/ had drifted from PLAN-0063's new public members
  (CallVerifier.AtLeast/AtMost, LogVerificationBuilder.AtLeast/AtMost,
  ReturnConfig<T>.ClearObservedCalls) - regenerated via
  .github/scripts/generate-api-reference.sh.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NiVv392P3m46azTD1TpU3s
ncipollina added a commit that referenced this pull request Sep 7, 2026
…nd ClearCalls() (#134)

* feat(testdoubles): add CallVerifier.AtLeast/AtMost, ReceivedCalls() and ClearCalls()

Adds AtLeast(int)/AtMost(int) count verification to core CallVerifier,
forwarded through Compono.Logging's LogVerificationBuilder, and reachable
from Compono.TestDoubles/Compono.Http with no package-side changes
(ADR-0044 Amendment 22). Adds Compono.TestDoubles generated ReceivedCalls()
(snapshot-based retrospective call inspection for ADR-0048-eligible
members) and whole-double ClearCalls() (observation reset preserving
configured behavior) per ADR-0060.

Includes generator/runtime changes, tests across all affected packages
(unit, generated-source snapshots, AOT smoke, concurrency), docs, skill
and eval updates, and a completed baseline-vs-updated eval comparison and
dogfood validation run, all tracked in PLAN-0063.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NiVv392P3m46azTD1TpU3s

* fix(testdoubles): resolve Codex-flagged generator collisions, fix package-validation CI, regen API docs

Three real generator-correctness bugs found by automated PR review, each with a
new generator-fixture regression test:

- ClearCalls()/ReceivedCalls() are always-emitted, always-zero-argument bridge
  extensions exactly like Configure()/Verify(), but the CMP0023 reserved-name
  collision check only covered the latter two - an interface declaring its own
  zero-argument ClearCalls/ReceivedCalls member silently shadowed the generated
  bridge with no diagnostic. Widened the reserved-name set in TestDoubleAnalyzer.
- An eligible member's generated {FieldName}_ReceivedCall record-class name could
  collide with an unrelated sibling member's own natural field name, producing a
  real CS0102 duplicate-declaration compile error. Fixed by feeding this derived
  name into the existing derivedAuxiliaryNameOwners collision pre-pass, which
  demotes the colliding member out of matching eligibility instead.
- A parameter literally named the same as its own member's generated record type
  produced a positional record property sharing the enclosing type's name
  (CS0542). Fixed in TestDouble.scriban by suffixing that one parameter's
  declared name inside the record declaration only.

Also fixes two unrelated CI blockers surfaced on this PR:
- .github/workflows/package-validation.yaml never set -p:Version on its
  validation-only pack, defaulting to 1.0.0.0 - this started failing ApiCompat's
  CP0003 the moment nuget.org's published baseline crossed 1.0.0
  (1.1.0-preview.103, published by PR #133's merge to main). Every PR's
  package-validation would fail this way regardless of content; pinned the
  packed Version to the resolved baseline.
- docs/reference/api/ had drifted from PLAN-0063's new public members
  (CallVerifier.AtLeast/AtMost, LogVerificationBuilder.AtLeast/AtMost,
  ReturnConfig<T>.ClearObservedCalls) - regenerated via
  .github/scripts/generate-api-reference.sh.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NiVv392P3m46azTD1TpU3s

* fix(testdoubles): make ReceivedCalls() record-parameter rename collision-free, not just naive

Codex review round 2 (PR #134) caught a real gap in the previous fix: the
scriban template's unconditional "_Value" suffix for a parameter colliding
with its own record's type name could itself collide with a second real
parameter already literally named that
(`Foo(int __Foo_ReceivedCall, int __Foo_ReceivedCall_Value)`), producing a
duplicate positional-record-property compile error.

Moved the rename computation into a new
TestDoubleMemberInfo.ReceivedCallRecordParametersText C# property, which
checks the candidate against every real parameter's own name (not just the
colliding one) and increments a numeric suffix until free
(__Foo_ReceivedCall_Value2, _Value3, ...). Wired through
TestDoubleEmitter.cs's anonymous projection model, which the first attempt
had missed - Scriban's default reflection binding only sees properties on
the wrapper anonymous object passed to Render(), not arbitrary computed
properties on the underlying TestDoubleMemberInfo record.

Added ReceivedCallRecordParameterNameCollidesWithBothRecordTypeAndItsNaiveRenameCandidate_ConsumerCompiles
covering exactly the two-parameter collision Codex identified.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NiVv392P3m46azTD1TpU3s

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

type: feat New feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants