Preserve IOutboundParameterTransformer on optional route constraints#65578
Preserve IOutboundParameterTransformer on optional route constraints#65578kubaflo wants to merge 2 commits intodotnet:mainfrom
Conversation
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>
|
Thanks for your PR, @@kubaflo. Someone from the team will get assigned to your PR shortly and we'll get it reviewed. |
There was a problem hiding this comment.
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 extendsOptionalRouteConstraintwhile also delegatingTransformOutboundto the inner constraint. - Updated
DefaultParameterPolicyFactory.InitializeRouteConstraintto use the new class when the inner constraint implementsIOutboundParameterTransformer. - Added agentic workflow skill files (
fix-issue,write-tests,verify-tests,try-fix,ai-summary-commentSKILL.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 |
| 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" |
There was a problem hiding this comment.
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.
| 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" |
There was a problem hiding this comment.
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.
| 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" |
| 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" |
There was a problem hiding this comment.
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.
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.