Skip to content

Preserve IOutboundParameterTransformer on optional route constraints#65578

Open
kubaflo wants to merge 2 commits intodotnet:mainfrom
kubaflo:fix/optional-route-constraint-transformoutbound-23063
Open

Preserve IOutboundParameterTransformer on optional route constraints#65578
kubaflo wants to merge 2 commits intodotnet:mainfrom
kubaflo:fix/optional-route-constraint-transformoutbound-23063

Conversation

@kubaflo
Copy link

@kubaflo kubaflo commented Mar 1, 2026

Fixes #23063 - TransformOutbound is not called when the route segment is optional. When a route constraint implements IOutboundParameterTransformer, wrapping it in OptionalRouteConstraint loses the transformer capability. Added OptionalOutboundParameterTransformerRouteConstraint that extends OptionalRouteConstraint and delegates TransformOutbound to the inner constraint.

kubaflo and others added 2 commits March 1, 2026 01:17
Introduce a set of GitHub "skills" and test helpers for an end-to-end fix workflow. Adds ai-summary-comment skill (SKILL.md) and a post-ai-summary-comment.sh script that builds/upserts a single unified AI Summary comment with nested <details> sections, dry-run support, and automatic state loading from CustomAgentLogsTmp/PRState. Adds a comprehensive fix-issue skill (SKILL.md) documenting a 4-phase workflow (pre-flight, test, gate, mandatory multi-model fix) plus try-fix, verify-tests, and write-tests skill definitions. Includes test scripts under fix-issue/tests to validate comment structure, skill definitions, and dry-run previews. These changes enable standardized outputs for automated agents and a consistent PR comment/reporting mechanism for multi-model exploration and fixes.
When a route constraint implements IOutboundParameterTransformer,
wrapping it in OptionalRouteConstraint loses the transformer capability
because OptionalRouteConstraint does not implement the interface.

Add OptionalOutboundParameterTransformerRouteConstraint that extends
OptionalRouteConstraint and delegates TransformOutbound to the inner
constraint. DefaultParameterPolicyFactory.InitializeRouteConstraint
now uses this wrapper when the inner constraint is a transformer.

Fixes dotnet#23063

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings March 1, 2026 02:06
@kubaflo kubaflo requested review from a team, halter73 and wtgodbe as code owners March 1, 2026 02:06
@github-actions github-actions bot added the needs-area-label Used by the dotnet-issue-labeler to label those issues which couldn't be triaged automatically label Mar 1, 2026
@dotnet-policy-service dotnet-policy-service bot added the community-contribution Indicates that the PR has been added by a community member label Mar 1, 2026
@dotnet-policy-service
Copy link
Contributor

Thanks for your PR, @@kubaflo. Someone from the team will get assigned to your PR shortly and we'll get it reviewed.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR addresses issue #23063 — when a route constraint implements IOutboundParameterTransformer and the route parameter is optional, wrapping the constraint in OptionalRouteConstraint caused the transformer to be silently dropped, resulting in incorrect URL generation.

Changes:

  • Introduced OptionalOutboundParameterTransformerRouteConstraint, an internal class that extends OptionalRouteConstraint while also delegating TransformOutbound to the inner constraint.
  • Updated DefaultParameterPolicyFactory.InitializeRouteConstraint to use the new class when the inner constraint implements IOutboundParameterTransformer.
  • Added agentic workflow skill files (fix-issue, write-tests, verify-tests, try-fix, ai-summary-comment SKILL.md and associated scripts/tests) — unrelated to the routing fix.

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/Http/Routing/src/Constraints/OptionalOutboundParameterTransformerRouteConstraint.cs New internal class that preserves IOutboundParameterTransformer behavior through optional constraint wrapping
src/Http/Routing/src/DefaultParameterPolicyFactory.cs Selects new wrapper class when inner constraint implements IOutboundParameterTransformer
src/Http/Routing/test/UnitTests/DefaultParameterPolicyFactoryTest.cs Regression test for the bug fix, including a TransformingRouteConstraint helper
.github/skills/fix-issue/SKILL.md Agentic workflow definition (5-phase issue fixer)
.github/skills/fix-issue/tests/test-skill-definition.sh Tests for the fix-issue SKILL.md — several assertions fail against current SKILL.md content
.github/skills/fix-issue/tests/test-ai-summary-comment.sh Tests for the ai-summary-comment scripts — references non-existent post-try-fix-comment.sh
.github/skills/ai-summary-comment/SKILL.md Skill definition for posting PR progress comments
.github/skills/ai-summary-comment/scripts/post-ai-summary-comment.sh Shell script that posts/updates the AI summary comment on a PR
.github/skills/write-tests/SKILL.md Skill definition for writing unit tests
.github/skills/verify-tests/SKILL.md Skill definition for verifying tests
.github/skills/try-fix/SKILL.md Skill definition for attempting alternative fixes

Comment on lines +12 to +78
TRY_FIX_SCRIPT="$REPO_ROOT/.github/skills/ai-summary-comment/scripts/post-try-fix-comment.sh"
SUMMARY_SCRIPT="$REPO_ROOT/.github/skills/ai-summary-comment/scripts/post-ai-summary-comment.sh"

PASS=0
FAIL=0
TEMP_DIR=""

setup_temp_fixtures() {
TEMP_DIR="$(mktemp -d)"
local issue_dir="$TEMP_DIR/CustomAgentLogsTmp/PRState/ISSUE-99999/PRAgent"

# Create try-fix directory structure
mkdir -p "$issue_dir/try-fix/attempt-1"
mkdir -p "$issue_dir/try-fix/attempt-2"
mkdir -p "$issue_dir/pre-flight"
mkdir -p "$issue_dir/gate"
mkdir -p "$issue_dir/report"

echo "Use try/catch to wrap await and throw TimeoutException" > "$issue_dir/try-fix/attempt-1/approach.md"
echo "PASS" > "$issue_dir/try-fix/attempt-1/result.txt"
cat > "$issue_dir/try-fix/attempt-1/fix.diff" << 'DIFF'
diff --git a/src/JSRuntime.cs b/src/JSRuntime.cs
--- a/src/JSRuntime.cs
+++ b/src/JSRuntime.cs
@@ -1,3 +1,5 @@
+try {
return await InvokeAsync(args);
+} catch (OperationCanceledException ex) when (cts.IsCancellationRequested) {
+ throw new TimeoutException("Timed out", ex);
+}
DIFF

echo "Use Task.WhenAny with delay task" > "$issue_dir/try-fix/attempt-2/approach.md"
echo "FAIL" > "$issue_dir/try-fix/attempt-2/result.txt"
echo "Test compilation failed due to missing reference" > "$issue_dir/try-fix/attempt-2/analysis.md"

# Create summary
cat > "$issue_dir/try-fix/content.md" << 'SUMMARY'
### Fix Exploration Summary
**Total Attempts:** 2
**Passing Candidates:** 1
**Selected Fix:** Attempt 1
| # | Model | Approach | Result |
|---|-------|----------|--------|
| 1 | claude-sonnet-4.6 | try/catch | ✅ PASS |
| 2 | claude-opus-4.6 | Task.WhenAny | ❌ FAIL |
SUMMARY

# Create phase files
echo "**Issue:** #99999 - Test issue" > "$issue_dir/pre-flight/content.md"
echo "### Gate Result: ✅ PASSED" > "$issue_dir/gate/content.md"
echo "### Final Report\n**PR:** #12345" > "$issue_dir/report/content.md"
}

cleanup() {
[[ -n "$TEMP_DIR" ]] && rm -rf "$TEMP_DIR"
}

trap cleanup EXIT

assert_ok() {
local description="$1"
local exit_code="$2"
if [[ "$exit_code" -eq 0 ]]; then
echo "$description"
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

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

The test script expects the file .github/skills/ai-summary-comment/scripts/post-try-fix-comment.sh to exist, but this file is not present in the repository. This will cause Test 1 to fail ("post-try-fix-comment.sh exists" assertion will always be false), and subsequent tests that attempt to invoke it (e.g., Test 2 dry-run) may also produce unexpected behavior.

Copilot uses AI. Check for mistakes.
Comment on lines +78 to +79
assert_file_exists "post-try-fix-comment.sh exists" "$SCRIPT_DIR/../../ai-summary-comment/scripts/post-try-fix-comment.sh"
assert_file_exists "post-ai-summary-comment.sh exists" "$SCRIPT_DIR/../../ai-summary-comment/scripts/post-ai-summary-comment.sh"
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

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

This assertion expects post-try-fix-comment.sh to exist at .github/skills/ai-summary-comment/scripts/post-try-fix-comment.sh, but that file does not exist in the repository. Additionally, line 126 asserts that fix-issue/SKILL.md contains a reference to post-try-fix-comment.sh, but searching the SKILL.md confirms it does not. These two assertions will both fail when the tests are run.

Suggested change
assert_file_exists "post-try-fix-comment.sh exists" "$SCRIPT_DIR/../../ai-summary-comment/scripts/post-try-fix-comment.sh"
assert_file_exists "post-ai-summary-comment.sh exists" "$SCRIPT_DIR/../../ai-summary-comment/scripts/post-ai-summary-comment.sh"

Copilot uses AI. Check for mistakes.
Comment on lines +89 to +137
assert_contains "Contains negative rule about skipping Phase 3" "$SKILL_FILE" "never skip phase 3"

MANDATORY_COUNT=$(count_occurrences "$SKILL_FILE" "mandatory")
if [[ "$MANDATORY_COUNT" -ge 3 ]]; then
echo " ✅ 'MANDATORY' appears at least 3 times ($MANDATORY_COUNT occurrences)"
PASS=$((PASS + 1))
else
echo " ❌ 'MANDATORY' appears only $MANDATORY_COUNT times (expected >= 3)"
FAIL=$((FAIL + 1))
fi

echo ""

# ============================================================================
echo "🤖 Test 3: All 5 models are specified"
# ============================================================================
assert_contains "Lists claude-sonnet-4.6" "$SKILL_FILE" "claude-sonnet-4.6"
assert_contains "Lists claude-opus-4.6" "$SKILL_FILE" "claude-opus-4.6"
assert_contains "Lists gpt-5.2" "$SKILL_FILE" "gpt-5.2"
assert_contains "Lists gpt-5.3-codex" "$SKILL_FILE" "gpt-5.3-codex"
assert_contains "Lists gemini-3-pro-preview" "$SKILL_FILE" "gemini-3-pro-preview"

echo ""

# ============================================================================
echo "🔄 Test 4: Cross-pollination is required"
# ============================================================================
assert_contains "Mentions cross-pollination" "$SKILL_FILE" "cross-pollination"
assert_contains "Requires at least 2 models for cross-pollination" "$SKILL_FILE" "at least 2 models"
assert_contains "Mentions 'NO NEW IDEAS' termination" "$SKILL_FILE" "NO NEW IDEAS"

echo ""

# ============================================================================
echo "💬 Test 5: AI Summary comment integration"
# ============================================================================
assert_contains "References ai-summary-comment skill" "$SKILL_FILE" "ai-summary-comment"
assert_contains "Includes post-try-fix-comment.sh command" "$SKILL_FILE" "post-try-fix-comment"
assert_contains "Includes post-ai-summary-comment.sh command" "$SKILL_FILE" "post-ai-summary-comment"
assert_contains "Comment posting is mandatory" "$SKILL_FILE" "MUST post"

echo ""

# ============================================================================
echo "📋 Test 6: All 4 phases are documented"
# ============================================================================
assert_contains "Phase 1: Pre-Flight documented" "$SKILL_FILE" "phase 1.*pre-flight"
assert_contains "Phase 2: Gate documented" "$SKILL_FILE" "phase 2.*gate"
assert_contains "Phase 3: Fix documented" "$SKILL_FILE" "phase 3.*fix"
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

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

Multiple test assertions will fail because they check for content that does not match the current fix-issue/SKILL.md:

  • Line 89: Checks for "never skip phase 3" — not found in SKILL.md (the document says "Never skip Phase 4 multi-model exploration")
  • Line 126: Checks for "post-try-fix-comment" — not found in SKILL.md
  • Line 128: Checks for "MUST post" — not found in SKILL.md
  • Line 136: Checks for "phase 2.*gate" — Phase 2 is "Test" in SKILL.md, not "Gate" (Gate is Phase 3)

These tests will always fail, making the test suite unreliable and possibly masking real regressions.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

community-contribution Indicates that the PR has been added by a community member needs-area-label Used by the dotnet-issue-labeler to label those issues which couldn't be triaged automatically

Projects

None yet

Development

Successfully merging this pull request may close these issues.

TransformOutbound of route constraint is not called when the route segment is optional

2 participants