Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 81 additions & 0 deletions .github/workflows/vi-mock-specifiers.yml
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
name: Inert vi.mock Specifiers

# Why this is its own workflow rather than a step in `ci.yml` or `lint.yml`: a
# module mock can be written into any package, in any shape of pull request, and
# both of those workflows decide inside the job whether the change "needs a full
# run" — with an exclusion list that skips every expensive step on a
# markdown-only or changeset-only change. This gate costs a checkout plus one
# `node` call, so there is nothing to gain by putting it behind that switch and
# a whole class of pull request to lose.
#
# Same shape and the same reasoning as `docs-links.yml`, `control-bytes.yml`,
# `skills-paths.yml`, `changeset-presence.yml` and `pre-install-import-graph.yml`,
# whose headers record the conclusion this repository has now reached five
# times: a gate that cannot see the pull request shape most likely to trip it
# "rebuilds the hole it exists to close". One gate, one home.
#
# Hence: no `paths` and no `paths-ignore` here, deliberately.
# `scripts/__tests__/check-vi-mock-specifiers.test.ts` fails if either is ever
# added, and fails too if a second workflow starts running the same script.
# Reporting on every pull request is also what makes the check requirable, and
# `scripts/dependabot-merge-gate.mjs` classifies it as a required context — an
# unclassified blocking check is one a Dependabot merge would be let past
# (objectui#6135).
#
# It needs no install and no build — a checkout plus one `node` call over ~3.7k
# tracked source files, measured at ~2.7s on this tree. Keep it that way; the
# import graph is builtins plus repo-relative modules only, which
# `pre-install-import-graph.yml` enforces (objectui#6148).

on:
pull_request:
branches: [main, develop]
push:
branches: [main, develop]
# Merge queue (objectui#3523 — see `ci.yml`'s trigger block for the full note
# and the measurements behind it). A required check that does not report on a
# queue build stalls the queue until the ruleset's 60-minute timeout fails it,
# so an unfiltered gate that can become required subscribes here from the
# start. `types:` is named although `checks_requested` is currently the only
# activity type GitHub defines for `merge_group`.
merge_group:
types: [checks_requested]
workflow_dispatch:

concurrency:
group: vi-mock-specifiers-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

permissions:
contents: read

jobs:
vi-mock-specifiers:
name: Inert vi.mock Specifier Check
runs-on: ubuntu-latest
timeout-minutes: 5

steps:
- name: Checkout code
uses: actions/checkout@v7

- name: Setup Node.js
uses: actions/setup-node@v7
with:
node-version: '22.x'

# A `vi.mock` whose relative specifier resolves to no file does NOT error.
# Vitest registers the mock against a module id nothing imports, the run
# proceeds with the real module everywhere, and the suite passes —
# identically to a correct one, with no warning and no smaller assertion
# count. objectui#5646: the one known instance (PR #5645) passed even with
# the code under test reverted to the shape the suite was written to
# catch, and only an ablation leg exposed it.
#
# This gate is GREEN AT REST — there are zero unresolvable specifiers in
# the tree and there should stay zero — so it prints its census rather
# than a bare "OK", and it FAILS if the population collapses to nothing.
# A scan that silently finds nothing reads as coverage, which is this
# gate's own defect one level up.
- name: Check every relative vi.mock specifier resolves
run: node scripts/check-vi-mock-specifiers.mjs
48 changes: 48 additions & 0 deletions content/docs/guide/ci-cd-pipeline.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -34,6 +34,7 @@ one has its own section below.
| `doc-snippet-types.yml` | Doc Snippet Type Check | Push / PR to `main`, `develop` — **no path filter**; merge-queue builds; manual | **Yes** — when a covered documentation snippet no longer compiles against the packages' built types |
| `doc-fence-languages.yml` | Doc Fence Language Check | Push / PR to `main`, `develop` — **no path filter**; merge-queue builds; manual | **Yes** — when a TypeScript block sits under a fence the snippet gate does not read |
| `pre-install-import-graph.yml` | Pre-Install Import Graph Check | Push / PR to `main`, `develop` — **no path filter**; merge-queue builds; manual | **Yes** — when a gate a workflow runs *before* `pnpm install` reaches a package anywhere in its import graph |
| `vi-mock-specifiers.yml` | Inert vi.mock Specifier Check | Push / PR to `main`, `develop` — **no path filter**; merge-queue builds; manual | **Yes** — when a `vi.mock` / `vi.doMock` relative specifier resolves to no file, or the scan's population collapses |
| `performance-budget.yml` | Bundle Analysis | Push / PR touching `packages/**`, `apps/console/**`, `pnpm-lock.yaml` | **Yes** — the console entry gzip budget |
| `live-e2e.yml` | Live E2E (informational) | PR to `main`, `develop` (code paths); nightly cron `30 6 * * *`; manual | No — informational lane, `continue-on-error` |
| `labeler.yml` | Auto Label PRs | PR `opened`, `synchronize`, `reopened` | No |
Expand DownExpand Up@@ -783,6 +784,53 @@ the import is deliberately *not* this gate's job: either drop the package, or mo
the derived population and every module walked, or `--self-test` to exercise the parser and the walk
against fixtures.

## Inert vi.mock Specifiers (`vi-mock-specifiers.yml`)

**Triggers:** Push and PR to `main`/`develop`, merge-queue builds, plus manual dispatch — with **no
path filter at all**. A module mock can be written into any package in any shape of pull request, and
the scan costs a checkout plus one `node` call, so there is nothing to gain by hiding it behind a
filter. It appears in the checks list as **Inert vi.mock Specifier Check**.

Runs `scripts/check-vi-mock-specifiers.mjs`. It walks every tracked JS/TS-family source file, finds
each `vi.mock` / `vi.doMock` call site, and resolves the **relative** specifiers against the calling
file's own directory. Any that resolves to no file fails the run.

**Why it needed a gate.** A mock whose specifier names no file does **not** error. Vitest registers
it against a module id nothing imports, the run proceeds with the *real* module everywhere, and the
suite passes — with no warning and no smaller assertion count, identically to a correct one. In
[#5646](https://github.com/objectstack-ai/objectui/issues/5646)'s one known instance (PR #5645) the
suite passed even when the code under test was reverted to the exact broken shape it had been written
to catch; only an ablation leg exposed it. Neighbouring mocks in that same file made it invisible to a
reader: one stepped up a single level and one stepped up two, and **both were correct**, because
their targets sat at different depths. This is
[#4347](https://github.com/objectstack-ai/objectui/issues/4347) one layer down — a declaration
pointing at nothing, reported as a pass.

**It is green at rest, so its census is part of the verdict.** There are zero unresolvable specifiers
in the tree and there should stay zero, which means the run's output alone cannot distinguish a
working gate from one that matches nothing. Two things answer that. The verdict line prints the
**population** it judged, not a bare `OK`. And the scan **fails when that population collapses**: no
source files, no test files, or no relative specifiers is a broken walk, not a clean tree, and
reporting `OK` for it would be this gate's own defect one level up. The evidence that the gate works
lives in `scripts/__tests__/check-vi-mock-specifiers.test.ts`, which reconstructs the historical
specifier on a fixture tree and pins that the two correct neighbours are *not* flagged.

**Resolution matches how this repo spells specifiers**, which is more than an existence check: the
bare path plus `.ts/.tsx/.js/.jsx/.mjs/.cjs`, the `/index.*` forms, and a trailing `.js` stripped and
retried, because `src/` is NodeNext throughout. The judgement is `isFile` rather than "exists", so a
directory with no index is correctly unresolved. Comments are masked and a call quoted inside a string
literal is counted but not judged — an ESLint `RuleTester` code sample is source text, not a mock.

**Scope:** relative specifiers only. A bare specifier (`@object-ui/…`, `lucide-react`) can be
misspelled too, but resolving one needs the workspace map rather than the filesystem — a different
check with a different failure mode. Bare specifiers are counted in the census and never judged.

**If it fails:** it names the file, the line and the specifier, and the first path it tried. Fix the
specifier, then confirm the mock is really installed by reverting the code under test and checking
that the suite goes red. Run it locally with `pnpm check:vi-mock-specifiers`, or
`node scripts/check-vi-mock-specifiers.mjs --list` to see every call site the walk found. It needs no
install and no build.

## Link Checking (`check-links.yml`)

**Trigger:** Weekly cron (`17 4 * * 0` — Sundays, off the top of the hour, when the scheduled-run
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -58,6 +58,7 @@
"check:eager-closure": "node scripts/check-eager-closure-budget.mjs",
"check:entry-guard": "node scripts/check-entry-guard.mjs",
"check:pre-install-import-graph": "node scripts/check-pre-install-import-graph.mjs",
"check:vi-mock-specifiers": "node scripts/check-vi-mock-specifiers.mjs",
"cli": "node packages/cli/dist/cli.js",
"objectui": "node packages/cli/dist/cli.js",
"create-plugin": "node packages/create-plugin/dist/index.js",
Expand Down
Loading
Loading