$schema: "@gobing-ai/spur/schemas/rule-file.schema.json"
# Guard against process-global module-mock leakage in bun:test.
#
# WHY: Bun's `mock.module()` installs a process-wide override that
# `mock.restore()` does NOT revert (per Bun docs). A `mock.module()` in one
# test file therefore leaks into every later file in the same `bun test`
# process. Because Bun discovers test files in filesystem order (sorted on
# macOS, inode order on Linux/CI), this surfaces as tests that pass locally
# but fail only in CI — exactly the 51-failure regression in
# apps/cli/tests/commands/{command,content-command-modules}.test.ts.
#
# SCOPE: only FIRST-PARTY mocks (relative `./`/`../` or `@gobing-ai/*` specifiers)
# are flagged. A `mock.module()` only leaks harmfully when another test file
# statically imports the SAME module expecting the real implementation — which only
# happens for the project's own modules. Third-party module swaps (e.g. `mermaid`,
# `@dnd-kit/core`, `@uiw/react-md-editor`) are never asserted-real by another test,
# so they cannot cause this failure and are intentionally NOT flagged.
#
# FIX: prefer `spyOn(namespaceImport, 'export')` over `mock.module()` — spyOn IS
# reverted by `mock.restore()`/`mockRestore()` in afterEach, so it cannot leak.
# CAVEAT: spyOn does NOT work on a callable Proxy (e.g. the oRPC `api` client) —
# property access regenerates the method, bypassing the spy and hitting the network.
# For such modules `mock.module()` is the only working option; keep it file-local and
# ensure no later test statically imports the real module (dynamic `await import()`
# in the reader is immune). This rule is a `warning`, not an `error`: treat a hit as
# "justify or convert," not an automatic block.
include:
  # Both .ts and .tsx — the React component tests (.tsx) are where mock.module is
  # most common, and they were previously invisible to this rule (the original globs
  # only matched .test.ts), letting a cross-file leak ship undetected.
  - "apps/**/tests/**/*.test.ts"
  - "apps/**/tests/**/*.test.tsx"
  - "packages/**/tests/**/*.test.ts"
  - "packages/**/tests/**/*.test.tsx"
exclude:
  # These three files mock.module() first-party modules and keep the mock inside a
  # runtime function guarded by beforeEach restoreMock() atop the shared full-surface
  # baseline (apps/web/tests/test-helpers/rpc-client-mock.ts, itself unflagged: it
  # only documents the surface in comments). Remaining mock.module() hits in the
  # test suite were verified stale or third-party-only and have been de-listed.
  - "apps/web/tests/modules/task-kanban/board.test.tsx"
  - "apps/web/tests/modules/task-kanban/new-task-panel.test.tsx"
  - "apps/web/tests/modules/task-kanban/task-detail.test.tsx"
  - "apps/web/tests/modules/projects/WorkView.test.tsx"
rules:
  - id: no-leaky-mock-module
    description: >
      Avoid `mock.module()` of a first-party module (relative path or @gobing-ai/*)
      in bun:test files — it is process-global, is not reverted by mock.restore(),
      and leaks into any later test file that statically imports the same module,
      causing CI-only, file-order-dependent failures. Prefer spyOn() on a namespace
      import (`import * as ns; spyOn(ns, 'fn')`), restored in afterEach. If the target
      is a callable Proxy (e.g. the oRPC api client) spyOn cannot intercept it; keep
      the mock.module() file-local and ensure readers use dynamic `await import()`.
      Third-party module swaps are not flagged (they never leak harmfully).
    severity: warning
    evaluator:
      type: rg
      config:
        # First-party only: line-anchored `mock.module(` whose specifier starts with a
        # relative path (`.`) or the `@gobing-ai/` scope. Third-party bare specifiers
        # (mermaid, @dnd-kit/*, @uiw/*) are excluded — they cannot cause the leak. The
        # line anchor keeps prose mentions in comments from false-positiving.
        pattern: "^\\s*mock\\.module\\(\\s*['\"](\\.|@gobing-ai/)"
        flags: "m"
