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-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/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..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 @@ -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..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 @@ -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..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 @@ -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-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 696863c2a..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 @@ -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..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 @@ -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..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 @@ -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..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 @@ -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..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 @@ -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-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/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-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 1400529d1..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 @@ -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..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 @@ -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/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 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: 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) + } +} 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()) + } +}