From 9f012fc04b7a24448a18fb567dd5c9a1cc59860a Mon Sep 17 00:00:00 2001 From: DevelopmentCats Date: Tue, 25 Nov 2025 14:47:00 -0600 Subject: [PATCH 1/9] feat: add test-check workflow --- .github/workflows/test-check.yaml | 227 ++++++++++++++++++++++++++++++ 1 file changed, 227 insertions(+) create mode 100644 .github/workflows/test-check.yaml diff --git a/.github/workflows/test-check.yaml b/.github/workflows/test-check.yaml new file mode 100644 index 0000000000000..56b4c6150f155 --- /dev/null +++ b/.github/workflows/test-check.yaml @@ -0,0 +1,227 @@ +# This workflow checks if a PR requires test updates. +# It creates a Coder Task that uses AI to analyze the PR changes, +# check existing tests, and comment with recommendations. +# +# Triggered by: Adding the "test-check" label to a PR, or manual dispatch. + +name: AI Test Coverage Check + +on: + pull_request: + types: + - labeled + workflow_dispatch: + inputs: + pr_url: + description: "Pull Request URL to check" + required: true + type: string + template_preset: + description: "Template preset to use" + required: false + default: "" + type: string + +jobs: + test-check: + name: Analyze PR for Test Updates Needed + runs-on: ubuntu-latest + if: | + (github.event.label.name == 'test-check' || github.event_name == 'workflow_dispatch') && + (github.event.pull_request.draft == false || github.event_name == 'workflow_dispatch') + timeout-minutes: 30 + env: + CODER_URL: ${{ secrets.DOC_CHECK_CODER_URL }} + CODER_SESSION_TOKEN: ${{ secrets.DOC_CHECK_CODER_SESSION_TOKEN }} + permissions: + contents: read + pull-requests: write + actions: write + + steps: + - name: Determine PR Context + id: determine-context + env: + GITHUB_ACTOR: ${{ github.actor }} + GITHUB_EVENT_NAME: ${{ github.event_name }} + GITHUB_EVENT_PR_HTML_URL: ${{ github.event.pull_request.html_url }} + GITHUB_EVENT_PR_NUMBER: ${{ github.event.pull_request.number }} + GITHUB_EVENT_SENDER_ID: ${{ github.event.sender.id }} + GITHUB_EVENT_SENDER_LOGIN: ${{ github.event.sender.login }} + INPUTS_PR_URL: ${{ inputs.pr_url }} + INPUTS_TEMPLATE_PRESET: ${{ inputs.template_preset || '' }} + GH_TOKEN: ${{ github.token }} + run: | + echo "Using template preset: ${INPUTS_TEMPLATE_PRESET}" + echo "template_preset=${INPUTS_TEMPLATE_PRESET}" >> "${GITHUB_OUTPUT}" + + # For workflow_dispatch, use the provided PR URL + if [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" ]]; then + if ! GITHUB_USER_ID=$(gh api "users/${GITHUB_ACTOR}" --jq '.id'); then + echo "::error::Failed to get GitHub user ID for actor ${GITHUB_ACTOR}" + exit 1 + fi + echo "Using workflow_dispatch actor: ${GITHUB_ACTOR} (ID: ${GITHUB_USER_ID})" + echo "github_user_id=${GITHUB_USER_ID}" >> "${GITHUB_OUTPUT}" + echo "github_username=${GITHUB_ACTOR}" >> "${GITHUB_OUTPUT}" + + echo "Using PR URL: ${INPUTS_PR_URL}" + # Convert /pull/ to /issues/ for create-task-action compatibility + ISSUE_URL="${INPUTS_PR_URL/\/pull\//\/issues\/}" + echo "pr_url=${ISSUE_URL}" >> "${GITHUB_OUTPUT}" + + # Extract PR number from URL for later use + PR_NUMBER=$(echo "${INPUTS_PR_URL}" | grep -oP '(?<=pull/)\d+') + echo "pr_number=${PR_NUMBER}" >> "${GITHUB_OUTPUT}" + + elif [[ "${GITHUB_EVENT_NAME}" == "pull_request" ]]; then + GITHUB_USER_ID=${GITHUB_EVENT_SENDER_ID} + echo "Using label adder: ${GITHUB_EVENT_SENDER_LOGIN} (ID: ${GITHUB_USER_ID})" + echo "github_user_id=${GITHUB_USER_ID}" >> "${GITHUB_OUTPUT}" + echo "github_username=${GITHUB_EVENT_SENDER_LOGIN}" >> "${GITHUB_OUTPUT}" + + echo "Using PR URL: ${GITHUB_EVENT_PR_HTML_URL}" + # Convert /pull/ to /issues/ for create-task-action compatibility + ISSUE_URL="${GITHUB_EVENT_PR_HTML_URL/\/pull\//\/issues\/}" + echo "pr_url=${ISSUE_URL}" >> "${GITHUB_OUTPUT}" + echo "pr_number=${GITHUB_EVENT_PR_NUMBER}" >> "${GITHUB_OUTPUT}" + + else + echo "::error::Unsupported event type: ${GITHUB_EVENT_NAME}" + exit 1 + fi + + - name: Build prompt + id: build-prompt + env: + PR_URL: ${{ steps.determine-context.outputs.pr_url }} + PR_NUMBER: ${{ steps.determine-context.outputs.pr_number }} + GH_TOKEN: ${{ github.token }} + run: | + echo "Analyzing PR #${PR_NUMBER}" + + # Build task prompt - using unquoted heredoc so variables expand + TASK_PROMPT=$(cat <> "${GITHUB_OUTPUT}" + + - name: Checkout create-task-action + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + with: + fetch-depth: 1 + path: ./.github/actions/create-task-action + persist-credentials: false + ref: main + repository: coder/create-task-action + + - name: Create Coder Task for Test Coverage Check + id: create_task + uses: ./.github/actions/create-task-action + with: + coder-url: ${{ secrets.DOC_CHECK_CODER_URL }} + coder-token: ${{ secrets.DOC_CHECK_CODER_SESSION_TOKEN }} + coder-organization: "default" + coder-template-name: coder + coder-template-preset: ${{ steps.determine-context.outputs.template_preset }} + coder-task-name-prefix: test-check + coder-task-prompt: ${{ steps.build-prompt.outputs.task_prompt }} + github-user-id: ${{ steps.determine-context.outputs.github_user_id }} + github-token: ${{ github.token }} + github-issue-url: ${{ steps.determine-context.outputs.pr_url }} + comment-on-issue: true + + - name: Write outputs + env: + TASK_CREATED: ${{ steps.create_task.outputs.task-created }} + TASK_NAME: ${{ steps.create_task.outputs.task-name }} + TASK_URL: ${{ steps.create_task.outputs.task-url }} + PR_URL: ${{ steps.determine-context.outputs.pr_url }} + run: | + { + echo "## Test Coverage Check Task" + echo "" + echo "**PR:** ${PR_URL}" + echo "**Task created:** ${TASK_CREATED}" + echo "**Task name:** ${TASK_NAME}" + echo "**Task URL:** ${TASK_URL}" + echo "" + echo "The Coder task is analyzing the PR changes and will comment with test coverage recommendations." + } >> "${GITHUB_STEP_SUMMARY}" + From 8004b9a7a2d45e0322412f6d55216f6f6eb75ba2 Mon Sep 17 00:00:00 2001 From: DevelopmentCats Date: Tue, 25 Nov 2025 14:59:34 -0600 Subject: [PATCH 2/9] chore: reinforce comment format in test-check workflow --- .github/workflows/test-check.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-check.yaml b/.github/workflows/test-check.yaml index 56b4c6150f155..085e20875ac16 100644 --- a/.github/workflows/test-check.yaml +++ b/.github/workflows/test-check.yaml @@ -142,7 +142,7 @@ jobs: NEEDS UPDATES: Changes that make existing test assertions incorrect or incomplete NO CHANGES: Tests already sufficient, or changes don't affect testable behavior - 6. Comment on the PR using this format + 6. Comment on the PR using EXACTLY this format (no additional sections) COMMENT FORMAT: ## 🧪 Test Coverage Review From db1183e24c70c54003f139a09dd204654a76d1e8 Mon Sep 17 00:00:00 2001 From: DevelopmentCats Date: Tue, 25 Nov 2025 15:08:25 -0600 Subject: [PATCH 3/9] chore: refine comment format for test coverage reviews in workflow --- .github/workflows/test-check.yaml | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test-check.yaml b/.github/workflows/test-check.yaml index 085e20875ac16..3e33a5e03504c 100644 --- a/.github/workflows/test-check.yaml +++ b/.github/workflows/test-check.yaml @@ -142,22 +142,26 @@ jobs: NEEDS UPDATES: Changes that make existing test assertions incorrect or incomplete NO CHANGES: Tests already sufficient, or changes don't affect testable behavior - 6. Comment on the PR using EXACTLY this format (no additional sections) + 6. Comment on the PR using this format (only include the section relevant to the findings) COMMENT FORMAT: ## 🧪 Test Coverage Review + If tests are needed: ### ❌ Tests Needed - - **path/to/file.go** - New function needs unit tests + - **path/to/file.go** - Brief description of what needs testing + If tests need updates: ### ⚠️ Tests Need Updates - - **path/to/file_test.go** - Assertions outdated for changed behavior + - **path/to/file_test.go** - Brief description of what needs updating + If tests should be removed: ### ⚠️ Tests to Remove - - **path/to/old_test.go** - Tests orphaned deleted code + - **path/to/old_test.go** - Brief description of why + If no changes needed: ### ✅ No Changes Needed - [Reason: Tests already updated in PR | Internal refactor with existing coverage | Docs-only changes | Config-only changes] + Brief reason (e.g., Tests already updated in PR | Internal refactor with existing coverage | Docs-only changes | Config-only changes) --- *This comment was generated by an AI Agent through [Coder Tasks](https://coder.com/docs/ai-coder/tasks)* From 1259ffa7626e3c598a1d699274e39461331c6c4f Mon Sep 17 00:00:00 2001 From: DevelopmentCats Date: Tue, 25 Nov 2025 15:16:56 -0600 Subject: [PATCH 4/9] chore: enhance comment clarity for test coverage reviews in workflow --- .github/workflows/test-check.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test-check.yaml b/.github/workflows/test-check.yaml index 3e33a5e03504c..a0fe177fb3038 100644 --- a/.github/workflows/test-check.yaml +++ b/.github/workflows/test-check.yaml @@ -149,19 +149,19 @@ jobs: If tests are needed: ### ❌ Tests Needed - - **path/to/file.go** - Brief description of what needs testing + - **path/to/file.go** - Description of what needs testing If tests need updates: ### ⚠️ Tests Need Updates - - **path/to/file_test.go** - Brief description of what needs updating + - **path/to/file_test.go** - Description of what needs updating If tests should be removed: ### ⚠️ Tests to Remove - - **path/to/old_test.go** - Brief description of why + - **path/to/old_test.go** - Description of why If no changes needed: ### ✅ No Changes Needed - Brief reason (e.g., Tests already updated in PR | Internal refactor with existing coverage | Docs-only changes | Config-only changes) + Reason (e.g., Tests already updated in PR | Internal refactor with existing coverage | Docs-only changes | Config-only changes) --- *This comment was generated by an AI Agent through [Coder Tasks](https://coder.com/docs/ai-coder/tasks)* From 174455cef5b2275246983ee2ad0119dd49da37b7 Mon Sep 17 00:00:00 2001 From: DevelopmentCats Date: Tue, 25 Nov 2025 15:30:36 -0600 Subject: [PATCH 5/9] chore: add clarification to comment on validation steps in test-check workflow --- .github/workflows/test-check.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/test-check.yaml b/.github/workflows/test-check.yaml index a0fe177fb3038..7efcfcd5a0529 100644 --- a/.github/workflows/test-check.yaml +++ b/.github/workflows/test-check.yaml @@ -163,6 +163,8 @@ jobs: ### ✅ No Changes Needed Reason (e.g., Tests already updated in PR | Internal refactor with existing coverage | Docs-only changes | Config-only changes) + Do not include validation steps or "how to test" instructions. + --- *This comment was generated by an AI Agent through [Coder Tasks](https://coder.com/docs/ai-coder/tasks)* From 76c458d3a4c63053b935b19edcb79237f8bca92f Mon Sep 17 00:00:00 2001 From: DevelopmentCats Date: Tue, 25 Nov 2025 16:34:52 -0600 Subject: [PATCH 6/9] test: new test required scenario --- coderd/util/strings/strings.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/coderd/util/strings/strings.go b/coderd/util/strings/strings.go index f320142da55a1..527692e774d52 100644 --- a/coderd/util/strings/strings.go +++ b/coderd/util/strings/strings.go @@ -94,6 +94,17 @@ func Truncate(s string, n int, opts ...TruncateOption) string { var bmPolicy = bluemonday.StrictPolicy() +// ContainsAny returns true if s contains any of the substrings in the list. +// The search is case-sensitive. +func ContainsAny(s string, substrings []string) bool { + for _, substr := range substrings { + if strings.Contains(s, substr) { + return true + } + } + return false +} + // UISanitize sanitizes a string for display in the UI. // The following transformations are applied, in order: // - HTML tags are removed using bluemonday's strict policy. From 7a8e66a7aa2790c2b48cf1af09cabcfa317b7499 Mon Sep 17 00:00:00 2001 From: DevelopmentCats Date: Tue, 25 Nov 2025 16:46:41 -0600 Subject: [PATCH 7/9] test: test update required scenario --- coderd/util/strings/strings.go | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/coderd/util/strings/strings.go b/coderd/util/strings/strings.go index 527692e774d52..a63445bf54402 100644 --- a/coderd/util/strings/strings.go +++ b/coderd/util/strings/strings.go @@ -10,10 +10,11 @@ import ( "github.com/microcosm-cc/bluemonday" ) -// EmptyToNil returns a `nil` for an empty string, or a pointer to the string -// otherwise. Useful when needing to treat zero values as nil in APIs. +// EmptyToNil returns a `nil` for an empty string or whitespace-only string, +// or a pointer to the string otherwise. Useful when needing to treat zero +// values as nil in APIs. func EmptyToNil(s string) *string { - if s == "" { + if strings.TrimSpace(s) == "" { return nil } return &s @@ -94,17 +95,6 @@ func Truncate(s string, n int, opts ...TruncateOption) string { var bmPolicy = bluemonday.StrictPolicy() -// ContainsAny returns true if s contains any of the substrings in the list. -// The search is case-sensitive. -func ContainsAny(s string, substrings []string) bool { - for _, substr := range substrings { - if strings.Contains(s, substr) { - return true - } - } - return false -} - // UISanitize sanitizes a string for display in the UI. // The following transformations are applied, in order: // - HTML tags are removed using bluemonday's strict policy. From 78e043487b6bc1595a5945123431074bc4c23812 Mon Sep 17 00:00:00 2001 From: DevelopmentCats Date: Tue, 25 Nov 2025 17:04:35 -0600 Subject: [PATCH 8/9] test: test update required scenario --- coderd/util/strings/strings.go | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/coderd/util/strings/strings.go b/coderd/util/strings/strings.go index a63445bf54402..88d94084ab109 100644 --- a/coderd/util/strings/strings.go +++ b/coderd/util/strings/strings.go @@ -10,24 +10,23 @@ import ( "github.com/microcosm-cc/bluemonday" ) -// EmptyToNil returns a `nil` for an empty string or whitespace-only string, -// or a pointer to the string otherwise. Useful when needing to treat zero -// values as nil in APIs. +// EmptyToNil returns a `nil` for an empty string, or a pointer to the string +// otherwise. Useful when needing to treat zero values as nil in APIs. func EmptyToNil(s string) *string { - if strings.TrimSpace(s) == "" { + if s == "" { return nil } return &s } // JoinWithConjunction joins a slice of strings with commas except for the last -// two which are joined with "and". +// two which are joined with "or". func JoinWithConjunction(s []string) string { last := len(s) - 1 if last == 0 { return s[last] } - return fmt.Sprintf("%s and %s", + return fmt.Sprintf("%s or %s", strings.Join(s[:last], ", "), s[last], ) From b4c190438c40bb5143698954b1b215909a04f6fc Mon Sep 17 00:00:00 2001 From: DevelopmentCats Date: Tue, 25 Nov 2025 17:11:47 -0600 Subject: [PATCH 9/9] chore: revert test scenario changes --- coderd/util/strings/strings.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/coderd/util/strings/strings.go b/coderd/util/strings/strings.go index 88d94084ab109..f320142da55a1 100644 --- a/coderd/util/strings/strings.go +++ b/coderd/util/strings/strings.go @@ -20,13 +20,13 @@ func EmptyToNil(s string) *string { } // JoinWithConjunction joins a slice of strings with commas except for the last -// two which are joined with "or". +// two which are joined with "and". func JoinWithConjunction(s []string) string { last := len(s) - 1 if last == 0 { return s[last] } - return fmt.Sprintf("%s or %s", + return fmt.Sprintf("%s and %s", strings.Join(s[:last], ", "), s[last], )