From 3249e2b4952d2c360edfec922d51c6a44fe5ad42 Mon Sep 17 00:00:00 2001 From: "Molecule AI Dev Engineer A (Kimi)" Date: Thu, 28 May 2026 03:36:00 +0000 Subject: [PATCH 1/6] =?UTF-8?q?ci(workflows):=20renew=20continue-on-error?= =?UTF-8?q?=20tracker=20mc#774=20=E2=86=92=20mc#1982?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit mc#774 reached its 14-day renewal cap, causing lint-continue-on-error-tracking to fail across all workflow PRs and making main red (#1975). Renew the forced- renewal tracker by creating mc#1982 and updating all 37 job-level mask comments. Affected: 34 workflow files with continue-on-error: true directives. Next renewal due: 2026-06-11. Fixes #1975 Refs: mc#774, mc#1982, feedback_chained_defects_in_never_tested_workflows Co-Authored-By: Claude Opus 4.7 --- .gitea/workflows/block-internal-paths.yml | 2 +- .gitea/workflows/check-migration-collisions.yml | 2 +- .gitea/workflows/ci-arm64-advisory.yml | 2 +- .gitea/workflows/ci.yml | 2 +- .gitea/workflows/continuous-synth-e2e.yml | 2 +- .gitea/workflows/e2e-api.yml | 4 ++-- .gitea/workflows/e2e-chat.yml | 4 ++-- .gitea/workflows/e2e-staging-canvas.yml | 4 ++-- .gitea/workflows/e2e-staging-external.yml | 2 +- .gitea/workflows/e2e-staging-saas.yml | 8 ++++---- .gitea/workflows/e2e-staging-sanity.yml | 2 +- .gitea/workflows/gate-check-v3.yml | 2 +- .gitea/workflows/handlers-postgres-integration.yml | 8 ++++---- .gitea/workflows/harness-replays.yml | 4 ++-- .gitea/workflows/lint-bp-context-emit-match.yml | 2 +- .gitea/workflows/lint-continue-on-error-tracking.yml | 10 +++++----- .gitea/workflows/lint-curl-status-capture.yml | 2 +- .gitea/workflows/lint-mask-pr-atomicity.yml | 4 ++-- .gitea/workflows/lint-pre-flip-continue-on-error.yml | 6 +++--- .../workflows/lint-required-context-exists-in-bp.yml | 4 ++-- .gitea/workflows/lint-workflow-yaml.yml | 2 +- .gitea/workflows/publish-canvas-image.yml | 2 +- .gitea/workflows/publish-workspace-server-image.yml | 2 +- .gitea/workflows/railway-pin-audit.yml | 2 +- .gitea/workflows/redeploy-tenants-on-main.yml | 2 +- .gitea/workflows/redeploy-tenants-on-staging.yml | 2 +- .gitea/workflows/review-check-tests.yml | 2 +- .gitea/workflows/secret-pattern-drift.yml | 2 +- .gitea/workflows/sop-tier-check.yml | 6 +++--- .gitea/workflows/staging-verify.yml | 4 ++-- .gitea/workflows/sweep-cf-orphans.yml | 2 +- .gitea/workflows/sweep-cf-tunnels.yml | 2 +- .gitea/workflows/test-ops-scripts.yml | 2 +- .gitea/workflows/weekly-platform-go.yml | 2 +- 34 files changed, 55 insertions(+), 55 deletions(-) diff --git a/.gitea/workflows/block-internal-paths.yml b/.gitea/workflows/block-internal-paths.yml index 8fff3bfec..f75524190 100644 --- a/.gitea/workflows/block-internal-paths.yml +++ b/.gitea/workflows/block-internal-paths.yml @@ -37,7 +37,7 @@ jobs: # Phase 3 (RFC #219 §1): surface broken workflows without blocking # the PR. Follow-up PR flips this off after surfaced defects are # triaged. - # mc#774: pre-existing continue-on-error mask; root-fix and remove, do not renew silently. + # mc#1982: pre-existing continue-on-error mask; root-fix and remove, do not renew silently. continue-on-error: true steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 diff --git a/.gitea/workflows/check-migration-collisions.yml b/.gitea/workflows/check-migration-collisions.yml index 991dd11a4..6441d7292 100644 --- a/.gitea/workflows/check-migration-collisions.yml +++ b/.gitea/workflows/check-migration-collisions.yml @@ -45,7 +45,7 @@ jobs: # Phase 3 (RFC #219 §1): surface broken workflows without blocking # the PR. Follow-up PR flips this off after surfaced defects are # triaged. - # mc#774: pre-existing continue-on-error mask; root-fix and remove, do not renew silently. + # mc#1982: pre-existing continue-on-error mask; root-fix and remove, do not renew silently. continue-on-error: true timeout-minutes: 5 steps: diff --git a/.gitea/workflows/ci-arm64-advisory.yml b/.gitea/workflows/ci-arm64-advisory.yml index 190f7e297..2422c5953 100644 --- a/.gitea/workflows/ci-arm64-advisory.yml +++ b/.gitea/workflows/ci-arm64-advisory.yml @@ -101,7 +101,7 @@ jobs: # AND-set: only the Mac arm64 runner advertises macos-self-hosted. # See "RUNNER TARGETING" header note for why bare self-hosted is unsafe. runs-on: [self-hosted, macos-self-hosted] - # ADVISORY: never blocks. See safety contract point 3. mc#774 + # ADVISORY: never blocks. See safety contract point 3. mc#1982 # internal#418 — tracked: arm64 advisory pilot, non-gating by design. continue-on-error: true # event_name gate: functional (only meaningful on push/PR) AND keeps diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index fff399810..ec29dbec6 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -161,7 +161,7 @@ jobs: echo "::group::pendinguploads exit=$pu_exit (last 100 lines)" tail -100 /tmp/test-pu.log echo "::endgroup::" - # mc#774: pre-existing continue-on-error mask; root-fix and remove, do not renew silently. + # mc#1982: pre-existing continue-on-error mask; root-fix and remove, do not renew silently. continue-on-error: true - if: ${{ needs.changes.outputs.platform == 'true' }} name: Run tests with coverage (blocking gate) diff --git a/.gitea/workflows/continuous-synth-e2e.yml b/.gitea/workflows/continuous-synth-e2e.yml index 569a11197..0e83706bb 100644 --- a/.gitea/workflows/continuous-synth-e2e.yml +++ b/.gitea/workflows/continuous-synth-e2e.yml @@ -102,7 +102,7 @@ jobs: name: Synthetic E2E against staging runs-on: ubuntu-latest # Phase 3 (RFC #219 §1): surface broken workflows without blocking. - # mc#774: pre-existing continue-on-error mask; root-fix and remove, do not renew silently. + # mc#1982: pre-existing continue-on-error mask; root-fix and remove, do not renew silently. continue-on-error: true # Bumped from 12 → 20 (2026-05-04). Tenant user-data install phase # (apt-get update + install docker.io/jq/awscli/caddy + snap install diff --git a/.gitea/workflows/e2e-api.yml b/.gitea/workflows/e2e-api.yml index 55fde08cd..468a53a70 100644 --- a/.gitea/workflows/e2e-api.yml +++ b/.gitea/workflows/e2e-api.yml @@ -123,7 +123,7 @@ jobs: # integration). See internal#512 for the class defect. runs-on: docker-host # Phase 3 (RFC #219 §1): surface broken workflows without blocking. - # mc#774: pre-existing continue-on-error mask; root-fix and remove, do not renew silently. + # mc#1982: pre-existing continue-on-error mask; root-fix and remove, do not renew silently. continue-on-error: true outputs: api: ${{ steps.decide.outputs.api }} @@ -160,7 +160,7 @@ jobs: # detect-changes for the full rationale. runs-on: docker-host # Phase 3 (RFC #219 §1): surface broken workflows without blocking. - # mc#774: pre-existing continue-on-error mask; root-fix and remove, do not renew silently. + # mc#1982: pre-existing continue-on-error mask; root-fix and remove, do not renew silently. continue-on-error: true timeout-minutes: 15 env: diff --git a/.gitea/workflows/e2e-chat.yml b/.gitea/workflows/e2e-chat.yml index 57b7da591..a186f5a3d 100644 --- a/.gitea/workflows/e2e-chat.yml +++ b/.gitea/workflows/e2e-chat.yml @@ -48,7 +48,7 @@ jobs: # defect. runs-on: docker-host # Phase 3 (RFC #219 §1): surface broken workflows without blocking. - # mc#774: pre-existing continue-on-error mask; root-fix and remove, do not renew silently. + # mc#1982: pre-existing continue-on-error mask; root-fix and remove, do not renew silently. continue-on-error: true outputs: chat: ${{ steps.decide.outputs.chat }} @@ -112,7 +112,7 @@ jobs: # Must land on operator-host Linux (docker-host). runs-on: docker-host # Phase 3 (RFC #219 §1): surface broken workflows without blocking. - # mc#774: pre-existing continue-on-error mask; root-fix and remove, do not renew silently. + # mc#1982: pre-existing continue-on-error mask; root-fix and remove, do not renew silently. continue-on-error: true timeout-minutes: 15 env: diff --git a/.gitea/workflows/e2e-staging-canvas.yml b/.gitea/workflows/e2e-staging-canvas.yml index 696863c2a..1a982b8ab 100644 --- a/.gitea/workflows/e2e-staging-canvas.yml +++ b/.gitea/workflows/e2e-staging-canvas.yml @@ -71,7 +71,7 @@ jobs: detect-changes: runs-on: ubuntu-latest # Phase 3 (RFC #219 §1): surface broken workflows without blocking. - # mc#774: pre-existing continue-on-error mask; root-fix and remove, do not renew silently. + # mc#1982: pre-existing continue-on-error mask; root-fix and remove, do not renew silently. continue-on-error: true outputs: canvas: ${{ steps.decide.outputs.canvas }} @@ -140,7 +140,7 @@ jobs: name: Canvas tabs E2E runs-on: ubuntu-latest # Phase 3 (RFC #219 §1): surface broken workflows without blocking. - # mc#774: pre-existing continue-on-error mask; root-fix and remove, do not renew silently. + # mc#1982: pre-existing continue-on-error mask; root-fix and remove, do not renew silently. continue-on-error: true timeout-minutes: 40 diff --git a/.gitea/workflows/e2e-staging-external.yml b/.gitea/workflows/e2e-staging-external.yml index 97d91aa55..8236e9b92 100644 --- a/.gitea/workflows/e2e-staging-external.yml +++ b/.gitea/workflows/e2e-staging-external.yml @@ -84,7 +84,7 @@ jobs: name: E2E Staging External Runtime runs-on: ubuntu-latest # Phase 3 (RFC #219 §1): surface broken workflows without blocking. - # mc#774: pre-existing continue-on-error mask; root-fix and remove, do not renew silently. + # mc#1982: pre-existing continue-on-error mask; root-fix and remove, do not renew silently. continue-on-error: true timeout-minutes: 25 diff --git a/.gitea/workflows/e2e-staging-saas.yml b/.gitea/workflows/e2e-staging-saas.yml index 315af9edc..b06c75e24 100644 --- a/.gitea/workflows/e2e-staging-saas.yml +++ b/.gitea/workflows/e2e-staging-saas.yml @@ -92,20 +92,20 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 1 - # mc#774: pre-existing continue-on-error mask; root-fix and remove, do not renew silently. + # mc#1982: pre-existing continue-on-error mask; root-fix and remove, do not renew silently. continue-on-error: true - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: "3.11" - # mc#774: pre-existing continue-on-error mask; root-fix and remove, do not renew silently. + # mc#1982: pre-existing continue-on-error mask; root-fix and remove, do not renew silently. continue-on-error: true - name: YAML validation (best-effort) run: | echo "e2e-staging-saas.yml — PR validation: workflow YAML is valid." echo "E2E step runs only when provisioning-critical files change." - # mc#774: pre-existing continue-on-error mask; root-fix and remove, do not renew silently. + # mc#1982: pre-existing continue-on-error mask; root-fix and remove, do not renew silently. continue-on-error: true # Actual E2E: runs on trunk pushes and PRs that touch provisioning-critical @@ -116,7 +116,7 @@ jobs: name: E2E Staging SaaS runs-on: ubuntu-latest # Phase 3 (RFC #219 §1): surface broken workflows without blocking. - # mc#774: pre-existing continue-on-error mask; root-fix and remove, do not renew silently. + # mc#1982: pre-existing continue-on-error mask; root-fix and remove, do not renew silently. continue-on-error: true timeout-minutes: 45 permissions: diff --git a/.gitea/workflows/e2e-staging-sanity.yml b/.gitea/workflows/e2e-staging-sanity.yml index d1b8f8eb9..8ca05f741 100644 --- a/.gitea/workflows/e2e-staging-sanity.yml +++ b/.gitea/workflows/e2e-staging-sanity.yml @@ -37,7 +37,7 @@ jobs: name: Intentional-failure teardown sanity runs-on: ubuntu-latest # Phase 3 (RFC #219 §1): surface broken workflows without blocking. - # mc#774: pre-existing continue-on-error mask; root-fix and remove, do not renew silently. + # mc#1982: pre-existing continue-on-error mask; root-fix and remove, do not renew silently. continue-on-error: true timeout-minutes: 20 diff --git a/.gitea/workflows/gate-check-v3.yml b/.gitea/workflows/gate-check-v3.yml index e8d603ecd..8b0acf8a4 100644 --- a/.gitea/workflows/gate-check-v3.yml +++ b/.gitea/workflows/gate-check-v3.yml @@ -66,7 +66,7 @@ jobs: # bp-exempt: PR advisory bot; merge blocking is enforced by CI status and branch protection. gate-check: runs-on: ubuntu-latest - # mc#774: pre-existing continue-on-error mask; root-fix and remove, do not renew silently. + # mc#1982: pre-existing continue-on-error mask; root-fix and remove, do not renew silently. continue-on-error: true # Never block on our own detector failing steps: - name: Check out BASE ref (never PR-head under pull_request_target) diff --git a/.gitea/workflows/handlers-postgres-integration.yml b/.gitea/workflows/handlers-postgres-integration.yml index 8ebfa0342..7c32de334 100644 --- a/.gitea/workflows/handlers-postgres-integration.yml +++ b/.gitea/workflows/handlers-postgres-integration.yml @@ -87,8 +87,8 @@ jobs: # both jobs on the same label avoids workspace-volume cross-host # surprises and keeps the routing rule discoverable in one place. runs-on: docker-host - # mc#774 Phase 3 (RFC §1): surface broken workflows without blocking. - # mc#774: pre-existing continue-on-error mask; root-fix and remove, do not renew silently. + # mc#1982 Phase 3 (RFC §1): surface broken workflows without blocking. + # mc#1982: pre-existing continue-on-error mask; root-fix and remove, do not renew silently. continue-on-error: true outputs: handlers: ${{ steps.filter.outputs.handlers }} @@ -118,8 +118,8 @@ jobs: # mc#1529 §1: must run on operator-host (where `molecule-core-net` # exists). See detect-changes for the full routing rationale. runs-on: docker-host - # mc#774 Phase 3 (RFC §1): surface broken workflows without blocking. - # mc#774: pre-existing continue-on-error mask; root-fix and remove, do not renew silently. + # mc#1982 Phase 3 (RFC §1): surface broken workflows without blocking. + # mc#1982: pre-existing continue-on-error mask; root-fix and remove, do not renew silently. continue-on-error: true env: # Unique name per run so concurrent jobs don't collide on the diff --git a/.gitea/workflows/harness-replays.yml b/.gitea/workflows/harness-replays.yml index 76559e2d2..580ee27a2 100644 --- a/.gitea/workflows/harness-replays.yml +++ b/.gitea/workflows/harness-replays.yml @@ -70,7 +70,7 @@ jobs: # of mc#1543; see internal#512 for class defect. runs-on: docker-host # Phase 3 (RFC #219 §1): surface broken workflows without blocking. - # mc#774: pre-existing continue-on-error mask; root-fix and remove, do not renew silently. + # mc#1982: pre-existing continue-on-error mask; root-fix and remove, do not renew silently. continue-on-error: true outputs: run: ${{ steps.decide.outputs.run }} @@ -172,7 +172,7 @@ jobs: # beta containers. Must run on operator-host Linux (docker-host). runs-on: docker-host # Phase 3 (RFC #219 §1): surface broken workflows without blocking. - # mc#774: pre-existing continue-on-error mask; root-fix and remove, do not renew silently. + # mc#1982: pre-existing continue-on-error mask; root-fix and remove, do not renew silently. continue-on-error: true timeout-minutes: 30 steps: diff --git a/.gitea/workflows/lint-bp-context-emit-match.yml b/.gitea/workflows/lint-bp-context-emit-match.yml index 702e305b9..6c9bf632e 100644 --- a/.gitea/workflows/lint-bp-context-emit-match.yml +++ b/.gitea/workflows/lint-bp-context-emit-match.yml @@ -94,7 +94,7 @@ jobs: # Phase 3 (RFC #219 §1): surface drift without blocking. After 7 # clean scheduled runs on main, flip to false so a scheduled # failure is a hard CI signal. - continue-on-error: true # mc#774 Phase 3 — flip to false after 7 clean main runs + continue-on-error: true # mc#1982 Phase 3 — flip to false after 7 clean main runs steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 diff --git a/.gitea/workflows/lint-continue-on-error-tracking.yml b/.gitea/workflows/lint-continue-on-error-tracking.yml index 8cb854bde..0dd0b16b6 100644 --- a/.gitea/workflows/lint-continue-on-error-tracking.yml +++ b/.gitea/workflows/lint-continue-on-error-tracking.yml @@ -1,6 +1,6 @@ name: lint-continue-on-error-tracking -# Tier 2e hard-gate lint (per mc#774) — every +# Tier 2e hard-gate lint (per mc#1982) — every # `continue-on-error: true` in `.gitea/workflows/*.yml` must carry a # `# mc#NNNN` or `# internal#NNNN` tracker comment within 2 lines, # the referenced issue must be OPEN, and ≤14 days old. @@ -8,7 +8,7 @@ name: lint-continue-on-error-tracking # Why this exists # --------------- # `continue-on-error: true` on `platform-build` had been hiding -# mc#774-class regressions for ~3 weeks before #656 surfaced them on +# mc#1982-class regressions for ~3 weeks before #656 surfaced them on # 2026-05-12. A 14-day cap on tracker age forces a review cycle and # surfaces mask-drift within at most 14 days of the original defect. # Each `continue-on-error: true` gets a paper trail — close or renew. @@ -97,9 +97,9 @@ jobs: # Phase 3 (RFC #219 §1): surface masked defects without blocking # PRs. Pre-existing continue-on-error: true directives on main # all violate this lint at first — intentional. Flip to false - # follow-up after main is clean for 3 days. mc#774. - # mc#774: pre-existing continue-on-error mask; root-fix and remove, do not renew silently. - continue-on-error: true # mc#774 Phase 3 mask — 14d forced-renewal cadence + # follow-up after main is clean for 3 days. mc#1982. + # mc#1982: pre-existing continue-on-error mask; root-fix and remove, do not renew silently. + continue-on-error: true # mc#1982 Phase 3 mask — 14d forced-renewal cadence steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 diff --git a/.gitea/workflows/lint-curl-status-capture.yml b/.gitea/workflows/lint-curl-status-capture.yml index e46371eff..0a5fd7dd9 100644 --- a/.gitea/workflows/lint-curl-status-capture.yml +++ b/.gitea/workflows/lint-curl-status-capture.yml @@ -51,7 +51,7 @@ jobs: # Phase 3 (RFC #219 §1): surface broken workflows without blocking # the PR. Follow-up PR flips this off after surfaced defects are # triaged. - # mc#774: pre-existing continue-on-error mask; root-fix and remove, do not renew silently. + # mc#1982: pre-existing continue-on-error mask; root-fix and remove, do not renew silently. continue-on-error: true steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 diff --git a/.gitea/workflows/lint-mask-pr-atomicity.yml b/.gitea/workflows/lint-mask-pr-atomicity.yml index 758d62b58..36f21e03f 100644 --- a/.gitea/workflows/lint-mask-pr-atomicity.yml +++ b/.gitea/workflows/lint-mask-pr-atomicity.yml @@ -92,8 +92,8 @@ jobs: # Phase 3 (RFC #219 §1): surface broken shapes without blocking # PRs. Follow-up PR flips this to `false` once recent runs on main # are confirmed clean (eat-our-own-dogfood discipline mirrors - # PR#673's same-shape comment). Tracking: mc#774. - # mc#774: pre-existing continue-on-error mask; root-fix and remove, do not renew silently. + # PR#673's same-shape comment). Tracking: mc#1982. + # mc#1982: pre-existing continue-on-error mask; root-fix and remove, do not renew silently. continue-on-error: true steps: - name: Check out PR head with full history (need base SHA blobs) diff --git a/.gitea/workflows/lint-pre-flip-continue-on-error.yml b/.gitea/workflows/lint-pre-flip-continue-on-error.yml index a1cbb6082..ead221d7d 100644 --- a/.gitea/workflows/lint-pre-flip-continue-on-error.yml +++ b/.gitea/workflows/lint-pre-flip-continue-on-error.yml @@ -4,7 +4,7 @@ name: Lint pre-flip continue-on-error # on any job in `.gitea/workflows/*.yml` WITHOUT proof that the affected # job's recent runs on the target branch (PR base) are actually green. # -# Empirical class: PR #656 / mc#774. PR #656 (RFC internal#219 Phase 4) +# Empirical class: PR #656 / mc#1982. PR #656 (RFC internal#219 Phase 4) # flipped 5 platform-build-class jobs `continue-on-error: true → false` # on the basis of a "verified green on main via combined-status check". # But that "green" was the LIE the prior `continue-on-error: true` @@ -99,8 +99,8 @@ jobs: timeout-minutes: 8 # Phase 3 (RFC internal#219 §1): surface broken flips without blocking # the PR yet. Follow-up flips this to `false` once the workflow itself - # has clean recent runs on main. mc#774 interim — remove when CoE→false. - continue-on-error: true # mc#774 + # has clean recent runs on main. mc#1982 interim — remove when CoE→false. + continue-on-error: true # mc#1982 steps: - name: Check out PR head (full history for base-SHA access) uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 diff --git a/.gitea/workflows/lint-required-context-exists-in-bp.yml b/.gitea/workflows/lint-required-context-exists-in-bp.yml index 45c7bc96d..695678fec 100644 --- a/.gitea/workflows/lint-required-context-exists-in-bp.yml +++ b/.gitea/workflows/lint-required-context-exists-in-bp.yml @@ -83,8 +83,8 @@ jobs: timeout-minutes: 5 # Phase 3 (RFC #219 §1): surface the pattern without blocking PRs # while the directive convention beds in. Follow-up flip to false - # after 7 clean days on main. mc#774. - continue-on-error: true # mc#774 Phase 3 — flip to false after 7 clean main runs + # after 7 clean days on main. mc#1982. + continue-on-error: true # mc#1982 Phase 3 — flip to false after 7 clean main runs steps: - name: Check out PR head with full history (need base SHA blobs) uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 diff --git a/.gitea/workflows/lint-workflow-yaml.yml b/.gitea/workflows/lint-workflow-yaml.yml index 5d2216de0..c8b48475a 100644 --- a/.gitea/workflows/lint-workflow-yaml.yml +++ b/.gitea/workflows/lint-workflow-yaml.yml @@ -55,7 +55,7 @@ jobs: # Phase 3 (RFC #219 §1): surface broken shapes without blocking PRs. # Follow-up PR flips this off after the 4 existing-on-main rule-2 # (workflow_run) violations are migrated to a supported trigger. - # mc#774: pre-existing continue-on-error mask; root-fix and remove, do not renew silently. + # mc#1982: pre-existing continue-on-error mask; root-fix and remove, do not renew silently. continue-on-error: true steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 diff --git a/.gitea/workflows/publish-canvas-image.yml b/.gitea/workflows/publish-canvas-image.yml index 12f37230d..e40e6b219 100644 --- a/.gitea/workflows/publish-canvas-image.yml +++ b/.gitea/workflows/publish-canvas-image.yml @@ -67,7 +67,7 @@ jobs: # in this rollout (internal#462) so the precondition holds. runs-on: publish # Phase 3 (RFC #219 §1): surface broken workflows without blocking. - # mc#774: pre-existing continue-on-error mask; root-fix and remove, do not renew silently. + # mc#1982: pre-existing continue-on-error mask; root-fix and remove, do not renew silently. continue-on-error: true steps: - name: Checkout diff --git a/.gitea/workflows/publish-workspace-server-image.yml b/.gitea/workflows/publish-workspace-server-image.yml index 6c21a1e5a..18862821e 100644 --- a/.gitea/workflows/publish-workspace-server-image.yml +++ b/.gitea/workflows/publish-workspace-server-image.yml @@ -234,7 +234,7 @@ jobs: name: Production auto-deploy needs: build-and-push if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} - # Side-effect deploy only; image publish success is the durable artifact. mc#774 + # Side-effect deploy only; image publish success is the durable artifact. mc#1982 continue-on-error: true # Publish/release lane (internal#462) — production deploy of a merged # fix; reserved capacity, never queued behind PR-CI. diff --git a/.gitea/workflows/railway-pin-audit.yml b/.gitea/workflows/railway-pin-audit.yml index 8508f4a87..569fa7d43 100644 --- a/.gitea/workflows/railway-pin-audit.yml +++ b/.gitea/workflows/railway-pin-audit.yml @@ -51,7 +51,7 @@ jobs: name: Audit Railway env vars for drift-prone pins runs-on: ubuntu-latest # Phase 3 (RFC #219 §1): surface broken workflows without blocking. - # mc#774: pre-existing continue-on-error mask; root-fix and remove, do not renew silently. + # mc#1982: pre-existing continue-on-error mask; root-fix and remove, do not renew silently. continue-on-error: true timeout-minutes: 10 diff --git a/.gitea/workflows/redeploy-tenants-on-main.yml b/.gitea/workflows/redeploy-tenants-on-main.yml index eec8ddfe2..1d6f76e61 100644 --- a/.gitea/workflows/redeploy-tenants-on-main.yml +++ b/.gitea/workflows/redeploy-tenants-on-main.yml @@ -73,7 +73,7 @@ jobs: # it never queues behind PR-CI. `publish` -> molecule-runner-publish-*. runs-on: publish # Phase 3 (RFC #219 §1): surface broken workflows without blocking. - # mc#774: pre-existing continue-on-error mask; root-fix and remove, do not renew silently. + # mc#1982: pre-existing continue-on-error mask; root-fix and remove, do not renew silently. continue-on-error: true timeout-minutes: 25 env: diff --git a/.gitea/workflows/redeploy-tenants-on-staging.yml b/.gitea/workflows/redeploy-tenants-on-staging.yml index a1283f78f..0960c556e 100644 --- a/.gitea/workflows/redeploy-tenants-on-staging.yml +++ b/.gitea/workflows/redeploy-tenants-on-staging.yml @@ -80,7 +80,7 @@ jobs: # `publish` -> molecule-runner-publish-* sub-pool. runs-on: publish # Phase 3 (RFC #219 §1): surface broken workflows without blocking. - # mc#774: pre-existing continue-on-error mask; root-fix and remove, do not renew silently. + # mc#1982: pre-existing continue-on-error mask; root-fix and remove, do not renew silently. continue-on-error: true timeout-minutes: 25 steps: diff --git a/.gitea/workflows/review-check-tests.yml b/.gitea/workflows/review-check-tests.yml index b60515ed5..4db3097d0 100644 --- a/.gitea/workflows/review-check-tests.yml +++ b/.gitea/workflows/review-check-tests.yml @@ -54,7 +54,7 @@ jobs: # runners with internet access to package mirrors). Falls back to GitHub # binary download. GitHub releases may be blocked on some runner networks # (infra#241 follow-up). - # mc#774: pre-existing continue-on-error mask; root-fix and remove, do not renew silently. + # mc#1982: pre-existing continue-on-error mask; root-fix and remove, do not renew silently. continue-on-error: true run: | if apt-get update -qq && apt-get install -y -qq jq; then diff --git a/.gitea/workflows/secret-pattern-drift.yml b/.gitea/workflows/secret-pattern-drift.yml index 879341ae4..723b7bb3f 100644 --- a/.gitea/workflows/secret-pattern-drift.yml +++ b/.gitea/workflows/secret-pattern-drift.yml @@ -57,7 +57,7 @@ jobs: name: Detect SECRET_PATTERNS drift runs-on: ubuntu-latest # Phase 3 (RFC #219 §1): surface broken workflows without blocking. - # mc#774: pre-existing continue-on-error mask; root-fix and remove, do not renew silently. + # mc#1982: pre-existing continue-on-error mask; root-fix and remove, do not renew silently. continue-on-error: true timeout-minutes: 5 steps: diff --git a/.gitea/workflows/sop-tier-check.yml b/.gitea/workflows/sop-tier-check.yml index c606aa4b3..8ee676ec4 100644 --- a/.gitea/workflows/sop-tier-check.yml +++ b/.gitea/workflows/sop-tier-check.yml @@ -36,7 +36,7 @@ # window closed. continue-on-error: true has been removed from the # tier-check job; AND-composition is now fully enforced. If you need # to temporarily re-introduce a mask, file a tracker and follow the -# mc#774 protocol (Tier 2e lint requires a current tracker within +# mc#1982 protocol (Tier 2e lint requires a current tracker within # 2 lines of any continue-on-error: true). name: sop-tier-check @@ -92,7 +92,7 @@ jobs: # runners). The sop-tier-check script has its own fallback as a # third line of defense. continue-on-error: true ensures this step # failing does not block the job. - # mc#774: pre-existing continue-on-error mask; root-fix and remove, do not renew silently. + # mc#1982: pre-existing continue-on-error mask; root-fix and remove, do not renew silently. continue-on-error: true run: | # apt-get is the primary method — Ubuntu package mirrors are reliably @@ -113,7 +113,7 @@ jobs: # continue-on-error: true at step level — job-level is ignored by Gitea # Actions (quirk #10, internal runbooks). Belt-and-suspenders with # SOP_FAIL_OPEN=1 + || true below. - # mc#774: pre-existing continue-on-error mask; root-fix and remove, do not renew silently. + # mc#1982: pre-existing continue-on-error mask; root-fix and remove, do not renew silently. continue-on-error: true env: GITEA_TOKEN: ${{ secrets.SOP_TIER_CHECK_TOKEN || secrets.GITHUB_TOKEN }} diff --git a/.gitea/workflows/staging-verify.yml b/.gitea/workflows/staging-verify.yml index 4c4af8976..1f2578e47 100644 --- a/.gitea/workflows/staging-verify.yml +++ b/.gitea/workflows/staging-verify.yml @@ -90,7 +90,7 @@ jobs: staging-smoke: runs-on: ubuntu-latest # Phase 3 (RFC #219 §1): surface broken workflows without blocking. - # mc#774: pre-existing continue-on-error mask; root-fix and remove, do not renew silently. + # mc#1982: pre-existing continue-on-error mask; root-fix and remove, do not renew silently. continue-on-error: true outputs: sha: ${{ steps.compute.outputs.sha }} @@ -212,7 +212,7 @@ jobs: if: ${{ needs.staging-smoke.result == 'success' && needs.staging-smoke.outputs.smoke_ran == 'true' }} runs-on: ubuntu-latest # Phase 3 (RFC #219 §1): surface broken workflows without blocking. - # mc#774: pre-existing continue-on-error mask; root-fix and remove, do not renew silently. + # mc#1982: pre-existing continue-on-error mask; root-fix and remove, do not renew silently. continue-on-error: true env: SHA: ${{ needs.staging-smoke.outputs.sha }} diff --git a/.gitea/workflows/sweep-cf-orphans.yml b/.gitea/workflows/sweep-cf-orphans.yml index 1400529d1..e7dc50f2a 100644 --- a/.gitea/workflows/sweep-cf-orphans.yml +++ b/.gitea/workflows/sweep-cf-orphans.yml @@ -71,7 +71,7 @@ jobs: name: Sweep CF orphans runs-on: ubuntu-latest # Phase 3 (RFC #219 §1): surface broken workflows without blocking. - # mc#774: pre-existing continue-on-error mask; root-fix and remove, do not renew silently. + # mc#1982: pre-existing continue-on-error mask; root-fix and remove, do not renew silently. continue-on-error: true # 3 min surfaces hangs (CF API stall, AWS describe-instances stuck) # within one cron interval instead of burning a full tick. Realistic diff --git a/.gitea/workflows/sweep-cf-tunnels.yml b/.gitea/workflows/sweep-cf-tunnels.yml index 085534e5d..fe7ab099f 100644 --- a/.gitea/workflows/sweep-cf-tunnels.yml +++ b/.gitea/workflows/sweep-cf-tunnels.yml @@ -55,7 +55,7 @@ jobs: name: Sweep CF tunnels runs-on: ubuntu-latest # Phase 3 (RFC #219 §1): surface broken workflows without blocking. - # mc#774: pre-existing continue-on-error mask; root-fix and remove, do not renew silently. + # mc#1982: pre-existing continue-on-error mask; root-fix and remove, do not renew silently. continue-on-error: true # 30 min cap. Was 5 min on the theory that the only thing that # could take >5min is a CF-API hang — but on 2026-05-02 a backlog diff --git a/.gitea/workflows/test-ops-scripts.yml b/.gitea/workflows/test-ops-scripts.yml index 59d321a58..a788eb72f 100644 --- a/.gitea/workflows/test-ops-scripts.yml +++ b/.gitea/workflows/test-ops-scripts.yml @@ -49,7 +49,7 @@ jobs: name: Ops scripts (unittest) runs-on: ubuntu-latest # Phase 3 (RFC #219 §1): surface broken workflows without blocking. - # mc#774: pre-existing continue-on-error mask; root-fix and remove, do not renew silently. + # mc#1982: pre-existing continue-on-error mask; root-fix and remove, do not renew silently. continue-on-error: true steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 diff --git a/.gitea/workflows/weekly-platform-go.yml b/.gitea/workflows/weekly-platform-go.yml index 63221e8e9..daee61f56 100644 --- a/.gitea/workflows/weekly-platform-go.yml +++ b/.gitea/workflows/weekly-platform-go.yml @@ -31,7 +31,7 @@ jobs: name: Weekly Platform-Go Surface runs-on: ubuntu-latest # continue-on-error: surface only, never block - # mc#774: pre-existing continue-on-error mask; root-fix and remove, do not renew silently. + # mc#1982: pre-existing continue-on-error mask; root-fix and remove, do not renew silently. continue-on-error: true defaults: run: -- 2.52.0 From f7204f963a152d3acff56179ea4c0268f67a4dae Mon Sep 17 00:00:00 2001 From: "Molecule AI Dev Engineer A (Kimi)" Date: Thu, 28 May 2026 02:13:50 +0000 Subject: [PATCH 2/6] =?UTF-8?q?ci(workflows):=20flip=20cancel-in-progress?= =?UTF-8?q?=20false=E2=86=92true=20on=2016=20workflows=20(#1357)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Gitea 1.22.6 does not honor cancel-in-progress: false for scheduled/push events — queued runs accumulate as stale scheduled tasks instead of waiting, saturating the runner pool (#1357). Flipping to true lets obsolete in-flight runs cancel correctly, freeing slots. Safe-flip set (PM + Eng B reviewed, 16 workflows): - ci-required-drift, staging-smoke, e2e-staging-sanity - sweep-cf-orphans, sweep-aws-secrets, sweep-cf-tunnels, sweep-stale-e2e-orgs - e2e-chat, e2e-legacy-advisory, e2e-peer-visibility, e2e-staging-canvas - continuous-synth-e2e, railway-pin-audit - handlers-postgres-integration, harness-replays, e2e-api Excluded (protected — half-rolled fleet / auto-promote / merge ordering): - e2e-staging-external, e2e-staging-saas, gitea-merge-queue - redeploy-tenants-on-staging, redeploy-tenants-on-main - main-red-watchdog, publish-workspace-server-image, status-reaper - gate-check-v3 Fixes #1357 Co-Authored-By: Claude Opus 4.7 --- .gitea/workflows/ci-required-drift.yml | 2 +- .gitea/workflows/continuous-synth-e2e.yml | 2 +- .gitea/workflows/e2e-api.yml | 2 +- .gitea/workflows/e2e-chat.yml | 2 +- .gitea/workflows/e2e-legacy-advisory.yml | 2 +- .gitea/workflows/e2e-peer-visibility.yml | 2 +- .gitea/workflows/e2e-staging-canvas.yml | 2 +- .gitea/workflows/e2e-staging-sanity.yml | 2 +- .gitea/workflows/handlers-postgres-integration.yml | 2 +- .gitea/workflows/harness-replays.yml | 2 +- .gitea/workflows/railway-pin-audit.yml | 2 +- .gitea/workflows/staging-smoke.yml | 2 +- .gitea/workflows/sweep-aws-secrets.yml | 2 +- .gitea/workflows/sweep-cf-orphans.yml | 2 +- .gitea/workflows/sweep-cf-tunnels.yml | 2 +- .gitea/workflows/sweep-stale-e2e-orgs.yml | 2 +- 16 files changed, 16 insertions(+), 16 deletions(-) diff --git a/.gitea/workflows/ci-required-drift.yml b/.gitea/workflows/ci-required-drift.yml index 3cf5e5dab..1f6965b31 100644 --- a/.gitea/workflows/ci-required-drift.yml +++ b/.gitea/workflows/ci-required-drift.yml @@ -57,7 +57,7 @@ permissions: # can produce duplicate comments before the title-search dedup wins. concurrency: group: ci-required-drift - cancel-in-progress: false + cancel-in-progress: true jobs: drift: diff --git a/.gitea/workflows/continuous-synth-e2e.yml b/.gitea/workflows/continuous-synth-e2e.yml index 0e83706bb..d60be04f9 100644 --- a/.gitea/workflows/continuous-synth-e2e.yml +++ b/.gitea/workflows/continuous-synth-e2e.yml @@ -92,7 +92,7 @@ permissions: # stacking up. concurrency: group: continuous-synth-e2e - cancel-in-progress: false + cancel-in-progress: true env: GITHUB_SERVER_URL: https://git.moleculesai.app diff --git a/.gitea/workflows/e2e-api.yml b/.gitea/workflows/e2e-api.yml index 468a53a70..6d935cabc 100644 --- a/.gitea/workflows/e2e-api.yml +++ b/.gitea/workflows/e2e-api.yml @@ -101,7 +101,7 @@ concurrency: # See e2e-staging-canvas.yml's identical concurrency block for the full # rationale and the 2026-04-28 incident reference. group: e2e-api-${{ github.event.pull_request.head.sha || github.sha }} - cancel-in-progress: false + cancel-in-progress: true env: GITHUB_SERVER_URL: https://git.moleculesai.app diff --git a/.gitea/workflows/e2e-chat.yml b/.gitea/workflows/e2e-chat.yml index a186f5a3d..57a6fe55b 100644 --- a/.gitea/workflows/e2e-chat.yml +++ b/.gitea/workflows/e2e-chat.yml @@ -32,7 +32,7 @@ on: concurrency: group: e2e-chat-${{ github.event.pull_request.head.sha || github.sha }} - cancel-in-progress: false + cancel-in-progress: true env: GITHUB_SERVER_URL: https://git.moleculesai.app diff --git a/.gitea/workflows/e2e-legacy-advisory.yml b/.gitea/workflows/e2e-legacy-advisory.yml index aeeb83f07..fc536fa0a 100644 --- a/.gitea/workflows/e2e-legacy-advisory.yml +++ b/.gitea/workflows/e2e-legacy-advisory.yml @@ -15,7 +15,7 @@ on: concurrency: group: e2e-legacy-advisory - cancel-in-progress: false + cancel-in-progress: true permissions: contents: read diff --git a/.gitea/workflows/e2e-peer-visibility.yml b/.gitea/workflows/e2e-peer-visibility.yml index fd2725717..da4d8fd55 100644 --- a/.gitea/workflows/e2e-peer-visibility.yml +++ b/.gitea/workflows/e2e-peer-visibility.yml @@ -115,7 +115,7 @@ concurrency: # would let a queued staging/main push behind a PR run get cancelled, # leaving any gate that reads "completed run at SHA" stuck. group: e2e-peer-visibility-${{ github.event.pull_request.head.sha || github.sha }} - cancel-in-progress: false + cancel-in-progress: true env: GITHUB_SERVER_URL: https://git.moleculesai.app diff --git a/.gitea/workflows/e2e-staging-canvas.yml b/.gitea/workflows/e2e-staging-canvas.yml index 1a982b8ab..2c98974cd 100644 --- a/.gitea/workflows/e2e-staging-canvas.yml +++ b/.gitea/workflows/e2e-staging-canvas.yml @@ -62,7 +62,7 @@ concurrency: # wasted CI is acceptable given the alternative is losing staging-tip # data that auto-promote-staging needs. group: e2e-staging-canvas-${{ github.event.pull_request.head.sha || github.sha }} - cancel-in-progress: false + cancel-in-progress: true env: GITHUB_SERVER_URL: https://git.moleculesai.app diff --git a/.gitea/workflows/e2e-staging-sanity.yml b/.gitea/workflows/e2e-staging-sanity.yml index 8ca05f741..22537fce6 100644 --- a/.gitea/workflows/e2e-staging-sanity.yml +++ b/.gitea/workflows/e2e-staging-sanity.yml @@ -26,7 +26,7 @@ env: concurrency: group: e2e-staging-sanity - cancel-in-progress: false + cancel-in-progress: true permissions: issues: write diff --git a/.gitea/workflows/handlers-postgres-integration.yml b/.gitea/workflows/handlers-postgres-integration.yml index 7c32de334..789aec137 100644 --- a/.gitea/workflows/handlers-postgres-integration.yml +++ b/.gitea/workflows/handlers-postgres-integration.yml @@ -69,7 +69,7 @@ on: branches: [main, staging] concurrency: group: handlers-pg-integ-${{ github.event.pull_request.head.sha || github.sha }} - cancel-in-progress: false + cancel-in-progress: true env: GITHUB_SERVER_URL: https://git.moleculesai.app diff --git a/.gitea/workflows/harness-replays.yml b/.gitea/workflows/harness-replays.yml index 580ee27a2..4e862e806 100644 --- a/.gitea/workflows/harness-replays.yml +++ b/.gitea/workflows/harness-replays.yml @@ -54,7 +54,7 @@ concurrency: # cancellation deadlock — see e2e-api.yml's concurrency block for # the 2026-04-28 incident that codified this pattern. group: harness-replays-${{ github.event.pull_request.head.sha || github.sha }} - cancel-in-progress: false + cancel-in-progress: true env: GITHUB_SERVER_URL: https://git.moleculesai.app diff --git a/.gitea/workflows/railway-pin-audit.yml b/.gitea/workflows/railway-pin-audit.yml index 569fa7d43..3003ab991 100644 --- a/.gitea/workflows/railway-pin-audit.yml +++ b/.gitea/workflows/railway-pin-audit.yml @@ -40,7 +40,7 @@ env: concurrency: group: railway-pin-audit - cancel-in-progress: false + cancel-in-progress: true permissions: issues: write diff --git a/.gitea/workflows/staging-smoke.yml b/.gitea/workflows/staging-smoke.yml index 9e3fce6a8..8b948e90d 100644 --- a/.gitea/workflows/staging-smoke.yml +++ b/.gitea/workflows/staging-smoke.yml @@ -38,7 +38,7 @@ on: # full run, but two smoke runs SHOULD queue against each other. concurrency: group: staging-smoke - cancel-in-progress: false + cancel-in-progress: true permissions: # Needed to open / close the alerting issue. diff --git a/.gitea/workflows/sweep-aws-secrets.yml b/.gitea/workflows/sweep-aws-secrets.yml index dcd00bfb6..63f1565c8 100644 --- a/.gitea/workflows/sweep-aws-secrets.yml +++ b/.gitea/workflows/sweep-aws-secrets.yml @@ -50,7 +50,7 @@ on: # Don't let two sweeps race the same AWS account. concurrency: group: sweep-aws-secrets - cancel-in-progress: false + cancel-in-progress: true permissions: contents: read diff --git a/.gitea/workflows/sweep-cf-orphans.yml b/.gitea/workflows/sweep-cf-orphans.yml index e7dc50f2a..b29598258 100644 --- a/.gitea/workflows/sweep-cf-orphans.yml +++ b/.gitea/workflows/sweep-cf-orphans.yml @@ -58,7 +58,7 @@ on: # scheduled run would otherwise issue duplicate DELETE calls. concurrency: group: sweep-cf-orphans - cancel-in-progress: false + cancel-in-progress: true permissions: contents: read diff --git a/.gitea/workflows/sweep-cf-tunnels.yml b/.gitea/workflows/sweep-cf-tunnels.yml index fe7ab099f..1641a311f 100644 --- a/.gitea/workflows/sweep-cf-tunnels.yml +++ b/.gitea/workflows/sweep-cf-tunnels.yml @@ -42,7 +42,7 @@ on: # Don't let two sweeps race the same account. concurrency: group: sweep-cf-tunnels - cancel-in-progress: false + cancel-in-progress: true permissions: contents: read diff --git a/.gitea/workflows/sweep-stale-e2e-orgs.yml b/.gitea/workflows/sweep-stale-e2e-orgs.yml index 8ba68fba7..f859e1896 100644 --- a/.gitea/workflows/sweep-stale-e2e-orgs.yml +++ b/.gitea/workflows/sweep-stale-e2e-orgs.yml @@ -51,7 +51,7 @@ on: # on a manual trigger; queue rather than parallel-delete. concurrency: group: sweep-stale-e2e-orgs - cancel-in-progress: false + cancel-in-progress: true permissions: contents: read -- 2.52.0 From ba1a9629a1c799a94ab36aa5284681492a3d918e Mon Sep 17 00:00:00 2001 From: "Molecule AI Dev Engineer A (Kimi)" Date: Thu, 28 May 2026 04:18:36 +0000 Subject: [PATCH 3/6] =?UTF-8?q?test(handlers):=20add=20PatchAbilities=20co?= =?UTF-8?q?verage=20=E2=80=94=20workspace=5Fabilities.go=20at=200%?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds 11 sqlmock-backed tests covering the PATCH /workspaces/:id/abilities handler (PatchAbilities): - Invalid workspace ID → 400 - Invalid JSON body → 400 - Empty body (no fields) → 400 - Workspace not found → 404 - Existence query error → 404 (fail-closed) - Patch broadcast_enabled only → 200 - Patch talk_to_user_enabled only → 200 - Patch both fields → 200 - DB error on broadcast update → 500 - DB error on talk_to_user update → 500 - DB error on broadcast when both supplied → 500 (partial update not committed) Closes #1312 Co-Authored-By: Claude Opus 4.7 --- .../handlers/workspace_abilities_test.go | 200 ++++++++++++++++++ 1 file changed, 200 insertions(+) create mode 100644 workspace-server/internal/handlers/workspace_abilities_test.go diff --git a/workspace-server/internal/handlers/workspace_abilities_test.go b/workspace-server/internal/handlers/workspace_abilities_test.go new file mode 100644 index 000000000..d2fc5a08c --- /dev/null +++ b/workspace-server/internal/handlers/workspace_abilities_test.go @@ -0,0 +1,200 @@ +package handlers + +// Sqlmock-backed coverage for workspace_abilities.go (PatchAbilities). +// Closes #1312 — handler was at 0% coverage. + +import ( + "bytes" + "errors" + "net/http" + "net/http/httptest" + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/gin-gonic/gin" +) + +func patchAbilitiesReq(t *testing.T, wsID string, body string) *httptest.ResponseRecorder { + t.Helper() + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + c.Params = gin.Params{{Key: "id", Value: wsID}} + c.Request = httptest.NewRequest("PATCH", "/workspaces/"+wsID+"/abilities", bytes.NewBufferString(body)) + c.Request.Header.Set("Content-Type", "application/json") + PatchAbilities(c) + return w +} + +// ---------- Validation errors ---------- + +func TestPatchAbilities_InvalidWorkspaceID(t *testing.T) { + w := patchAbilitiesReq(t, "not-a-uuid", `{"broadcast_enabled":true}`) + if w.Code != http.StatusBadRequest { + t.Fatalf("expected 400, got %d: %s", w.Code, w.Body.String()) + } +} + +func TestPatchAbilities_InvalidJSON(t *testing.T) { + w := patchAbilitiesReq(t, wsUUID1, `not json`) + if w.Code != http.StatusBadRequest { + t.Fatalf("expected 400, got %d: %s", w.Code, w.Body.String()) + } +} + +func TestPatchAbilities_EmptyBody(t *testing.T) { + w := patchAbilitiesReq(t, wsUUID1, `{}`) + if w.Code != http.StatusBadRequest { + t.Fatalf("expected 400, got %d: %s", w.Code, w.Body.String()) + } +} + +// ---------- Not found ---------- + +func TestPatchAbilities_WorkspaceNotFound(t *testing.T) { + mock, cleanup := withMockDB(t) + defer cleanup() + + mock.ExpectQuery(`SELECT EXISTS\(SELECT 1 FROM workspaces WHERE id = \$1 AND status != 'removed'\)`). + WithArgs(wsUUID1). + WillReturnRows(sqlmock.NewRows([]string{"exists"}).AddRow(false)) + + w := patchAbilitiesReq(t, wsUUID1, `{"broadcast_enabled":true}`) + if w.Code != http.StatusNotFound { + t.Fatalf("expected 404, got %d: %s", w.Code, w.Body.String()) + } + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("unmet: %v", err) + } +} + +func TestPatchAbilities_ExistsQueryError(t *testing.T) { + mock, cleanup := withMockDB(t) + defer cleanup() + + mock.ExpectQuery(`SELECT EXISTS\(SELECT 1 FROM workspaces WHERE id = \$1 AND status != 'removed'\)`). + WithArgs(wsUUID1). + WillReturnError(errors.New("conn refused")) + + w := patchAbilitiesReq(t, wsUUID1, `{"broadcast_enabled":true}`) + if w.Code != http.StatusNotFound { + t.Fatalf("expected 404 on exists query error, got %d: %s", w.Code, w.Body.String()) + } +} + +// ---------- Happy paths ---------- + +func TestPatchAbilities_BroadcastOnly(t *testing.T) { + mock, cleanup := withMockDB(t) + defer cleanup() + + mock.ExpectQuery(`SELECT EXISTS\(SELECT 1 FROM workspaces WHERE id = \$1 AND status != 'removed'\)`). + WithArgs(wsUUID1). + WillReturnRows(sqlmock.NewRows([]string{"exists"}).AddRow(true)) + mock.ExpectExec(`UPDATE workspaces SET broadcast_enabled = \$2, updated_at = now\(\) WHERE id = \$1`). + WithArgs(wsUUID1, true). + WillReturnResult(sqlmock.NewResult(0, 1)) + + w := patchAbilitiesReq(t, wsUUID1, `{"broadcast_enabled":true}`) + if w.Code != http.StatusOK { + t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String()) + } + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("unmet: %v", err) + } +} + +func TestPatchAbilities_TalkToUserOnly(t *testing.T) { + mock, cleanup := withMockDB(t) + defer cleanup() + + mock.ExpectQuery(`SELECT EXISTS\(SELECT 1 FROM workspaces WHERE id = \$1 AND status != 'removed'\)`). + WithArgs(wsUUID1). + WillReturnRows(sqlmock.NewRows([]string{"exists"}).AddRow(true)) + mock.ExpectExec(`UPDATE workspaces SET talk_to_user_enabled = \$2, updated_at = now\(\) WHERE id = \$1`). + WithArgs(wsUUID1, false). + WillReturnResult(sqlmock.NewResult(0, 1)) + + w := patchAbilitiesReq(t, wsUUID1, `{"talk_to_user_enabled":false}`) + if w.Code != http.StatusOK { + t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String()) + } + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("unmet: %v", err) + } +} + +func TestPatchAbilities_BothFields(t *testing.T) { + mock, cleanup := withMockDB(t) + defer cleanup() + + mock.ExpectQuery(`SELECT EXISTS\(SELECT 1 FROM workspaces WHERE id = \$1 AND status != 'removed'\)`). + WithArgs(wsUUID1). + WillReturnRows(sqlmock.NewRows([]string{"exists"}).AddRow(true)) + mock.ExpectExec(`UPDATE workspaces SET broadcast_enabled = \$2, updated_at = now\(\) WHERE id = \$1`). + WithArgs(wsUUID1, true). + WillReturnResult(sqlmock.NewResult(0, 1)) + mock.ExpectExec(`UPDATE workspaces SET talk_to_user_enabled = \$2, updated_at = now\(\) WHERE id = \$1`). + WithArgs(wsUUID1, true). + WillReturnResult(sqlmock.NewResult(0, 1)) + + w := patchAbilitiesReq(t, wsUUID1, `{"broadcast_enabled":true,"talk_to_user_enabled":true}`) + if w.Code != http.StatusOK { + t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String()) + } + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("unmet: %v", err) + } +} + +// ---------- DB errors on update ---------- + +func TestPatchAbilities_BroadcastUpdateError(t *testing.T) { + mock, cleanup := withMockDB(t) + defer cleanup() + + mock.ExpectQuery(`SELECT EXISTS\(SELECT 1 FROM workspaces WHERE id = \$1 AND status != 'removed'\)`). + WithArgs(wsUUID1). + WillReturnRows(sqlmock.NewRows([]string{"exists"}).AddRow(true)) + mock.ExpectExec(`UPDATE workspaces SET broadcast_enabled = \$2, updated_at = now\(\) WHERE id = \$1`). + WithArgs(wsUUID1, true). + WillReturnError(errors.New("disk full")) + + w := patchAbilitiesReq(t, wsUUID1, `{"broadcast_enabled":true}`) + if w.Code != http.StatusInternalServerError { + t.Fatalf("expected 500, got %d: %s", w.Code, w.Body.String()) + } +} + +func TestPatchAbilities_TalkToUserUpdateError(t *testing.T) { + mock, cleanup := withMockDB(t) + defer cleanup() + + mock.ExpectQuery(`SELECT EXISTS\(SELECT 1 FROM workspaces WHERE id = \$1 AND status != 'removed'\)`). + WithArgs(wsUUID1). + WillReturnRows(sqlmock.NewRows([]string{"exists"}).AddRow(true)) + mock.ExpectExec(`UPDATE workspaces SET talk_to_user_enabled = \$2, updated_at = now\(\) WHERE id = \$1`). + WithArgs(wsUUID1, false). + WillReturnError(errors.New("disk full")) + + w := patchAbilitiesReq(t, wsUUID1, `{"talk_to_user_enabled":false}`) + if w.Code != http.StatusInternalServerError { + t.Fatalf("expected 500, got %d: %s", w.Code, w.Body.String()) + } +} + +func TestPatchAbilities_BothFields_BroadcastFails(t *testing.T) { + mock, cleanup := withMockDB(t) + defer cleanup() + + mock.ExpectQuery(`SELECT EXISTS\(SELECT 1 FROM workspaces WHERE id = \$1 AND status != 'removed'\)`). + WithArgs(wsUUID1). + WillReturnRows(sqlmock.NewRows([]string{"exists"}).AddRow(true)) + mock.ExpectExec(`UPDATE workspaces SET broadcast_enabled = \$2, updated_at = now\(\) WHERE id = \$1`). + WithArgs(wsUUID1, true). + WillReturnError(errors.New("disk full")) + + w := patchAbilitiesReq(t, wsUUID1, `{"broadcast_enabled":true,"talk_to_user_enabled":true}`) + if w.Code != http.StatusInternalServerError { + t.Fatalf("expected 500, got %d: %s", w.Code, w.Body.String()) + } +} -- 2.52.0 From 2b03f22656a4b7f38ad36d11863b5e9e069196db Mon Sep 17 00:00:00 2001 From: "Molecule AI Dev Engineer A (Kimi)" Date: Thu, 28 May 2026 04:35:45 +0000 Subject: [PATCH 4/6] =?UTF-8?q?test(handlers):=20add=20org=5Fscope=20cover?= =?UTF-8?q?age=20=E2=80=94=20orgRootID=20+=20sameOrg=20at=200%=20(#1953)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds 10 sqlmock-backed tests covering the cross-tenant isolation helpers introduced in #1953. Covered: - orgRootID: happy path (child→root), workspace-is-root, no rows, DB error, empty root string - sameOrg: identical IDs (short-circuit), same org root, different org roots, orgRootID fails, orgRootID not found Closes #1953 follow-up (test debt) Co-Authored-By: Claude Opus 4.7 --- .../internal/handlers/org_scope_test.go | 191 ++++++++++++++++++ 1 file changed, 191 insertions(+) create mode 100644 workspace-server/internal/handlers/org_scope_test.go diff --git a/workspace-server/internal/handlers/org_scope_test.go b/workspace-server/internal/handlers/org_scope_test.go new file mode 100644 index 000000000..032c10ee0 --- /dev/null +++ b/workspace-server/internal/handlers/org_scope_test.go @@ -0,0 +1,191 @@ +package handlers + +// Sqlmock-backed coverage for org_scope.go (orgRootID + sameOrg). +// Security-critical path — cross-tenant isolation (#1953). + +import ( + "context" + "errors" + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "git.moleculesai.app/molecule-ai/molecule-core/workspace-server/internal/db" +) + +// ---------- orgRootID ---------- + +func TestOrgRootID_HappyPath_NonRoot(t *testing.T) { + mock, cleanup := withMockDB(t) + defer cleanup() + + // CTE walks: ws-child → ws-parent → org-root (parent_id IS NULL) + mock.ExpectQuery(`WITH RECURSIVE org_chain`). + WithArgs(wsUUID1). + WillReturnRows(sqlmock.NewRows([]string{"root_id"}).AddRow(wsUUID3)) + + root, err := orgRootID(context.Background(), db.DB, wsUUID1) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if root != wsUUID3 { + t.Errorf("root=%q, want %q", root, wsUUID3) + } + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("unmet: %v", err) + } +} + +func TestOrgRootID_WorkspaceIsRoot(t *testing.T) { + mock, cleanup := withMockDB(t) + defer cleanup() + + // One-row chain: the workspace itself is the org root. + mock.ExpectQuery(`WITH RECURSIVE org_chain`). + WithArgs(wsUUID1). + WillReturnRows(sqlmock.NewRows([]string{"root_id"}).AddRow(wsUUID1)) + + root, err := orgRootID(context.Background(), db.DB, wsUUID1) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if root != wsUUID1 { + t.Errorf("root=%q, want %q", root, wsUUID1) + } +} + +func TestOrgRootID_NoRows(t *testing.T) { + mock, cleanup := withMockDB(t) + defer cleanup() + + mock.ExpectQuery(`WITH RECURSIVE org_chain`). + WithArgs(wsUUID1). + WillReturnRows(sqlmock.NewRows([]string{"root_id"})) + + _, err := orgRootID(context.Background(), db.DB, wsUUID1) + if !errors.Is(err, errNoOrgRoot) { + t.Fatalf("expected errNoOrgRoot, got %v", err) + } +} + +func TestOrgRootID_DBError(t *testing.T) { + mock, cleanup := withMockDB(t) + defer cleanup() + + mock.ExpectQuery(`WITH RECURSIVE org_chain`). + WithArgs(wsUUID1). + WillReturnError(errors.New("conn lost")) + + _, err := orgRootID(context.Background(), db.DB, wsUUID1) + if err == nil || errors.Is(err, errNoOrgRoot) { + t.Fatalf("expected DB error, got %v", err) + } +} + +func TestOrgRootID_EmptyRoot(t *testing.T) { + mock, cleanup := withMockDB(t) + defer cleanup() + + // Row present but root is empty string → treated as not-found. + mock.ExpectQuery(`WITH RECURSIVE org_chain`). + WithArgs(wsUUID1). + WillReturnRows(sqlmock.NewRows([]string{"root_id"}).AddRow("")) + + _, err := orgRootID(context.Background(), db.DB, wsUUID1) + if !errors.Is(err, errNoOrgRoot) { + t.Fatalf("expected errNoOrgRoot for empty root, got %v", err) + } +} + +// ---------- sameOrg ---------- + +func TestSameOrg_SameWorkspace(t *testing.T) { + // Fast path: identical IDs are same-org without touching DB. + mock, cleanup := withMockDB(t) + defer cleanup() + + ok, err := sameOrg(context.Background(), db.DB, wsUUID1, wsUUID1) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !ok { + t.Error("same workspace must be same-org") + } + // No DB expectations → proves short-circuit. + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("DB was touched despite short-circuit: %v", err) + } +} + +func TestSameOrg_SameOrg(t *testing.T) { + mock, cleanup := withMockDB(t) + defer cleanup() + + mock.ExpectQuery(`WITH RECURSIVE org_chain`). + WithArgs(wsUUID1). + WillReturnRows(sqlmock.NewRows([]string{"root_id"}).AddRow(wsUUID3)) + mock.ExpectQuery(`WITH RECURSIVE org_chain`). + WithArgs(wsUUID2). + WillReturnRows(sqlmock.NewRows([]string{"root_id"}).AddRow(wsUUID3)) + + ok, err := sameOrg(context.Background(), db.DB, wsUUID1, wsUUID2) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !ok { + t.Error("expected same-org") + } + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("unmet: %v", err) + } +} + +func TestSameOrg_DifferentOrg(t *testing.T) { + mock, cleanup := withMockDB(t) + defer cleanup() + + mock.ExpectQuery(`WITH RECURSIVE org_chain`). + WithArgs(wsUUID1). + WillReturnRows(sqlmock.NewRows([]string{"root_id"}).AddRow(wsUUID3)) + mock.ExpectQuery(`WITH RECURSIVE org_chain`). + WithArgs(wsUUID2). + WillReturnRows(sqlmock.NewRows([]string{"root_id"}).AddRow("org-b")) + + ok, err := sameOrg(context.Background(), db.DB, wsUUID1, wsUUID2) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if ok { + t.Error("expected different-org") + } + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("unmet: %v", err) + } +} + +func TestSameOrg_OrgRootFails(t *testing.T) { + mock, cleanup := withMockDB(t) + defer cleanup() + + mock.ExpectQuery(`WITH RECURSIVE org_chain`). + WithArgs(wsUUID1). + WillReturnError(errors.New("conn lost")) + + _, err := sameOrg(context.Background(), db.DB, wsUUID1, wsUUID2) + if err == nil { + t.Fatal("expected error when orgRootID fails") + } +} + +func TestSameOrg_OrgRootNotFound(t *testing.T) { + mock, cleanup := withMockDB(t) + defer cleanup() + + mock.ExpectQuery(`WITH RECURSIVE org_chain`). + WithArgs(wsUUID1). + WillReturnRows(sqlmock.NewRows([]string{"root_id"})) + + _, err := sameOrg(context.Background(), db.DB, wsUUID1, wsUUID2) + if !errors.Is(err, errNoOrgRoot) { + t.Fatalf("expected errNoOrgRoot, got %v", err) + } +} -- 2.52.0 From 10ecc31e754f4fc53d31751a41a35154ec408bc1 Mon Sep 17 00:00:00 2001 From: "Molecule AI Dev Engineer A (Kimi)" Date: Thu, 28 May 2026 04:45:41 +0000 Subject: [PATCH 5/6] fix(broadcast): port corrected org-root CTE from org_scope.go (#1959) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The broadcast handler's org-root lookup CTE carried `id AS root_id` from the recursive seed. For a non-root sender this resolved the org root to the sender itself instead of its topmost ancestor, causing broadcasts to under-deliver (only the sender's own subtree received the message, missing siblings and the org root). Port the corrected CTE shape from org_scope.go (#1954): - Seed selects only `id, parent_id` (no carried root_id). - Final SELECT reads `id AS root_id` from the row whose `parent_id IS NULL` — the actual org root. The recipient query CTE (walking DOWN from parent_id=NULL) was already correct and is untouched. Closes #1959 Co-Authored-By: Claude Opus 4.7 --- .../internal/handlers/workspace_broadcast.go | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/workspace-server/internal/handlers/workspace_broadcast.go b/workspace-server/internal/handlers/workspace_broadcast.go index 27ce7a8ea..1aef881c8 100644 --- a/workspace-server/internal/handlers/workspace_broadcast.go +++ b/workspace-server/internal/handlers/workspace_broadcast.go @@ -82,18 +82,23 @@ func (h *BroadcastHandler) Broadcast(c *gin.Context) { // Find the sender's org root by walking the parent_id chain. // Workspaces with parent_id = NULL are org roots; every other workspace // belongs to the org identified by its topmost ancestor. + // + // NOTE: this uses the corrected CTE from org_scope.go (#1954). The old + // shape carried `id AS root_id` from the recursive seed, which caused a + // non-root sender to resolve to itself rather than its org root, making + // broadcasts under-deliver (miss the rest of the org). See #1959. var orgRootID string err = db.DB.QueryRowContext(ctx, ` WITH RECURSIVE org_chain AS ( - SELECT id, parent_id, id AS root_id + SELECT id, parent_id FROM workspaces WHERE id = $1 UNION ALL - SELECT w.id, w.parent_id, c.root_id + SELECT w.id, w.parent_id FROM workspaces w JOIN org_chain c ON w.id = c.parent_id ) - SELECT root_id FROM org_chain WHERE parent_id IS NULL LIMIT 1 + SELECT id AS root_id FROM org_chain WHERE parent_id IS NULL LIMIT 1 `, senderID).Scan(&orgRootID) if err != nil { log.Printf("Broadcast: org root lookup for %s: %v", senderID, err) -- 2.52.0 From b3ad9753151100fe9d5dbbb2f1043a70866969fb Mon Sep 17 00:00:00 2001 From: "Molecule AI Dev Engineer A (Kimi)" Date: Thu, 28 May 2026 04:54:42 +0000 Subject: [PATCH 6/6] =?UTF-8?q?test(handlers):=20cover=20QueueDepth=20+=20?= =?UTF-8?q?QueueStatusByID=20=E2=80=94=20a2a=20queue=20at=200%=20(#1870)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds 6 sqlmock-backed tests covering previously-uncovered queue helpers: - QueueDepth (a2a_queue.go): - Happy path returns correct count - Query error returns 0 (fail-open informational) - QueueStatusByID (a2a_queue_status.go): - Happy path with all nullable fields populated - No rows → sql.ErrNoRows - NULL optionals projected as nil pointers - DB error propagated Co-Authored-By: Claude Opus 4.7 --- .../handlers/a2a_queue_status_test.go | 124 ++++++++++++++++++ .../internal/handlers/a2a_queue_test.go | 38 ++++++ 2 files changed, 162 insertions(+) diff --git a/workspace-server/internal/handlers/a2a_queue_status_test.go b/workspace-server/internal/handlers/a2a_queue_status_test.go index 5b71c360f..07bb4de36 100644 --- a/workspace-server/internal/handlers/a2a_queue_status_test.go +++ b/workspace-server/internal/handlers/a2a_queue_status_test.go @@ -2,6 +2,8 @@ package handlers import ( "context" + "database/sql" + "errors" "testing" "github.com/DATA-DOG/go-sqlmock" @@ -111,3 +113,125 @@ func TestExtractExpiresInSeconds(t *testing.T) { }) } } + +// TestQueueStatusByID_HappyPath verifies the full projection including optional +// nullable fields and response_body surfacing when status == completed. +func TestQueueStatusByID_HappyPath(t *testing.T) { + mock := setupTestDB(t) + queueID := "queue-789" + + mock.ExpectQuery(`SELECT\s+q\.id,\s+q\.workspace_id,\s+q\.status,\s+q\.priority,\s+q\.attempts,\s+q\.last_error,\s+q\.enqueued_at::text,\s+q\.dispatched_at::text,\s+q\.completed_at::text,\s+q\.expires_at::text,\s+al\.response_body::text\s+FROM a2a_queue q\s+LEFT JOIN activity_logs al`). + WithArgs(queueID). + WillReturnRows(sqlmock.NewRows([]string{ + "id", "workspace_id", "status", "priority", "attempts", + "last_error", "enqueued_at", "dispatched_at", "completed_at", "expires_at", + "response_body", + }).AddRow( + queueID, "ws-target", "completed", 50, 2, + "previous error", "2026-05-28T10:00:00Z", "2026-05-28T10:01:00Z", "2026-05-28T10:02:00Z", "2026-05-28T11:00:00Z", + []byte(`{"result":"ok"}`), + )) + + qs, err := QueueStatusByID(context.Background(), queueID) + if err != nil { + t.Fatalf("QueueStatusByID returned error: %v", err) + } + if qs.ID != queueID { + t.Errorf("ID = %q, want %q", qs.ID, queueID) + } + if qs.Status != "completed" { + t.Errorf("Status = %q, want completed", qs.Status) + } + if qs.LastError == nil || *qs.LastError != "previous error" { + t.Errorf("LastError = %v, want 'previous error'", qs.LastError) + } + if qs.DispatchedAt == nil || *qs.DispatchedAt != "2026-05-28T10:01:00Z" { + t.Errorf("DispatchedAt = %v", qs.DispatchedAt) + } + if qs.CompletedAt == nil || *qs.CompletedAt != "2026-05-28T10:02:00Z" { + t.Errorf("CompletedAt = %v", qs.CompletedAt) + } + if qs.ExpiresAt == nil || *qs.ExpiresAt != "2026-05-28T11:00:00Z" { + t.Errorf("ExpiresAt = %v", qs.ExpiresAt) + } + if string(qs.ResponseBody) != `{"result":"ok"}` { + t.Errorf("ResponseBody = %q", qs.ResponseBody) + } + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("unmet expectations: %v", err) + } +} + +// TestQueueStatusByID_NoRows returns sql.ErrNoRows when the queue id does not exist. +func TestQueueStatusByID_NoRows(t *testing.T) { + mock := setupTestDB(t) + queueID := "queue-missing" + + mock.ExpectQuery(`SELECT\s+q\.id,\s+q\.workspace_id,\s+q\.status,\s+q\.priority,\s+q\.attempts,\s+q\.last_error,\s+q\.enqueued_at::text,\s+q\.dispatched_at::text,\s+q\.completed_at::text,\s+q\.expires_at::text,\s+al\.response_body::text\s+FROM a2a_queue q\s+LEFT JOIN activity_logs`). + WithArgs(queueID). + WillReturnRows(sqlmock.NewRows([]string{ + "id", "workspace_id", "status", "priority", "attempts", + "last_error", "enqueued_at", "dispatched_at", "completed_at", "expires_at", + "response_body", + })) + + _, err := QueueStatusByID(context.Background(), queueID) + if !errors.Is(err, sql.ErrNoRows) { + t.Fatalf("expected sql.ErrNoRows, got %v", err) + } +} + +// TestQueueStatusByID_NullOptionals confirms that NULL dispatched_at / completed_at / +// expires_at / last_error are projected as nil pointers, and response_body is NOT +// included when status != completed. +func TestQueueStatusByID_NullOptionals(t *testing.T) { + mock := setupTestDB(t) + queueID := "queue-nulls" + + mock.ExpectQuery(`SELECT\s+q\.id,\s+q\.workspace_id,\s+q\.status,\s+q\.priority,\s+q\.attempts,\s+q\.last_error,\s+q\.enqueued_at::text,\s+q\.dispatched_at::text,\s+q\.completed_at::text,\s+q\.expires_at::text,\s+al\.response_body::text\s+FROM a2a_queue q\s+LEFT JOIN activity_logs`). + WithArgs(queueID). + WillReturnRows(sqlmock.NewRows([]string{ + "id", "workspace_id", "status", "priority", "attempts", + "last_error", "enqueued_at", "dispatched_at", "completed_at", "expires_at", + "response_body", + }).AddRow( + queueID, "ws-target", "queued", 50, 0, + nil, "2026-05-28T10:00:00Z", nil, nil, nil, + nil, + )) + + qs, err := QueueStatusByID(context.Background(), queueID) + if err != nil { + t.Fatalf("QueueStatusByID returned error: %v", err) + } + if qs.LastError != nil { + t.Errorf("LastError = %v, want nil", qs.LastError) + } + if qs.DispatchedAt != nil { + t.Errorf("DispatchedAt = %v, want nil", qs.DispatchedAt) + } + if qs.CompletedAt != nil { + t.Errorf("CompletedAt = %v, want nil", qs.CompletedAt) + } + if qs.ExpiresAt != nil { + t.Errorf("ExpiresAt = %v, want nil", qs.ExpiresAt) + } + if qs.ResponseBody != nil { + t.Errorf("ResponseBody = %q, want nil for non-completed status", qs.ResponseBody) + } +} + +// TestQueueStatusByID_DBError surfaces the underlying error on unexpected failure. +func TestQueueStatusByID_DBError(t *testing.T) { + mock := setupTestDB(t) + queueID := "queue-dberr" + + mock.ExpectQuery(`SELECT\s+q\.id,\s+q\.workspace_id,\s+q\.status,\s+q\.priority,\s+q\.attempts,\s+q\.last_error,\s+q\.enqueued_at::text,\s+q\.dispatched_at::text,\s+q\.completed_at::text,\s+q\.expires_at::text,\s+al\.response_body::text\s+FROM a2a_queue q\s+LEFT JOIN activity_logs`). + WithArgs(queueID). + WillReturnError(errors.New("disk full")) + + _, err := QueueStatusByID(context.Background(), queueID) + if err == nil || errors.Is(err, sql.ErrNoRows) { + t.Fatalf("expected DB error, got %v", err) + } +} diff --git a/workspace-server/internal/handlers/a2a_queue_test.go b/workspace-server/internal/handlers/a2a_queue_test.go index 93c6b6629..753f1665b 100644 --- a/workspace-server/internal/handlers/a2a_queue_test.go +++ b/workspace-server/internal/handlers/a2a_queue_test.go @@ -12,6 +12,7 @@ package handlers import ( "context" "database/sql" + "errors" "fmt" "net/http" "net/http/httptest" @@ -520,3 +521,40 @@ func TestDrainQueueForWorkspace_ClaimGuarding_SecondDrainGetsEmpty(t *testing.T) t.Errorf("unmet sqlmock expectations: %v", err) } } + +// ────────────────────────────────────────────────────────────────────────────── +// QueueDepth +// ────────────────────────────────────────────────────────────────────────────── + +func TestQueueDepth_HappyPath(t *testing.T) { + mock := setupTestDBForQueueTests(t) + wsID := "ws-depth-1" + + mock.ExpectQuery("SELECT COUNT(*) FROM a2a_queue WHERE workspace_id = $1 AND status = 'queued'"). + WithArgs(wsID). + WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(7)) + + if got := QueueDepth(context.Background(), wsID); got != 7 { + t.Errorf("QueueDepth = %d, want 7", got) + } + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("unmet expectations: %v", err) + } +} + +func TestQueueDepth_QueryError(t *testing.T) { + mock := setupTestDBForQueueTests(t) + wsID := "ws-depth-2" + + mock.ExpectQuery("SELECT COUNT(*) FROM a2a_queue WHERE workspace_id = $1 AND status = 'queued'"). + WithArgs(wsID). + WillReturnError(errors.New("conn lost")) + + // Must return 0 (fail-open informational) rather than panic or propagate. + if got := QueueDepth(context.Background(), wsID); got != 0 { + t.Errorf("QueueDepth on error = %d, want 0", got) + } + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("unmet expectations: %v", err) + } +} -- 2.52.0