> ## Documentation Index
> Fetch the complete documentation index at: https://docs.superoffice.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Automated tests

> Every check that runs against a PR or on a schedule for this repo - markdownlint, Vale, build validation, broken-link checking, the CRMScript reference drift check, and the stale generated-pages check - what each covers, whether it blocks a merge, and where to look when one flags something.

This page is the single reference for every automatic check that runs against this repo, whether it is triggered by a pull request or runs on its own schedule. It does not duplicate the general mechanics already covered in [Mintlify deployment and CI/CD][1] - only what each check covers and how to act on it.

## Per-PR checks

| Check                                    | Runs via                                                                  | Blocking?                                                       | Configured in                                                                          |
| ---------------------------------------- | ------------------------------------------------------------------------- | --------------------------------------------------------------- | -------------------------------------------------------------------------------------- |
| Database enum sidebarTitle auto-fix      | GitHub Action (`.github/workflows/database-enum-sidebar-title.yml`)       | No - auto-fixes and pushes a commit                             | This repo, `tools/ci/add-database-enum-sidebar-titles.py`                              |
| MDO-provider title/sidebarTitle auto-fix | GitHub Action (`.github/workflows/mdo-provider-sidebar-title.yml`)        | No - auto-fixes and pushes a commit                             | This repo, `tools/ci/add-mdo-provider-sidebar-titles.py`                               |
| Reserved `version` frontmatter auto-fix  | GitHub Action (`.github/workflows/version-frontmatter-guard.yml`)         | No - auto-fixes and pushes a commit                             | This repo, `tools/ci/fix-reserved-version-property.py`                                 |
| Reference-page overflow auto-fix         | GitHub Action (`.github/workflows/reference-overflow-guard.yml`)          | No - auto-fixes and pushes a commit                             | This repo, `tools/ci/apply-wide-mode.py` + `tools/ci/apply-code-wrap.py`               |
| OpenAPI conversion auto-fix              | GitHub Action (`.github/workflows/openapi-conversion-guard.yml`)          | No - auto-fixes and pushes a commit                             | This repo, `tools/convert-swagger-to-openapi.ps1` + `tools/ci/generate-openapi-nav.py` |
| Recent-pages sync auto-fix               | GitHub Action (`.github/workflows/recent-pages-sync.yml`)                 | No - auto-fixes and pushes a commit                             | This repo, `tools/ci/sync-recent-pages.py`                                             |
| Markdownlint                             | GitHub Action (`.github/workflows/markdownlint.yml`)                      | No - advisory                                                   | This repo, `.markdownlint.yaml` / `.markdownlintignore`                                |
| Landing page template guard              | GitHub Action (`.github/workflows/landing-page-template-guard.yml`)       | No - advisory (warns only)                                      | This repo, `tools/ci/check-landing-page-templates.py`                                  |
| Release-notes API structure guard        | GitHub Action (`.github/workflows/release-notes-api-structure-guard.yml`) | No - advisory (warns only)                                      | This repo, `tools/ci/check-release-notes-api-structure.py`                             |
| Vale                                     | Mintlify CI Checks add-on                                                 | No - Warning level                                              | Mintlify dashboard (Add-ons > CI Checks), using this repo's `.vale.ini`                |
| Build validation                         | Mintlify preview deployment                                               | Yes - a failed build shows as a red "Mintlify Deployment" check | Mintlify, automatic on every PR (see [Deploy trigger mechanics][2])                    |
| Broken links (internal)                  | Mintlify CI Checks add-on                                                 | No - Warning level                                              | Mintlify dashboard (Add-ons > CI Checks)                                               |
| CRMScript reference drift                | GitHub Action (`.github/workflows/crmscript-drift.yml`)                   | Yes - fails the check                                           | This repo, `tools/ci/check-crmscript-drift.ps1`                                        |
| Stale generated pages                    | GitHub Action (`.github/workflows/stale-generated-pages.yml`)             | Yes - fails the check                                           | This repo, `tools/ci/find-stale-generated-pages.py`                                    |

<Note>
  Every auto-fix check on this page shares one behavior worth knowing about, first confirmed live on #272: because this repo is public, a workflow's own `GITHUB_TOKEN` push lands its own re-triggered run in "action required" instead of running automatically - GitHub gates it behind a one-click manual approval (Actions tab > the pending run > **Approve and run**) rather than silently re-running. This isn't a failure and doesn't mean anything is broken: the fix commit has already landed by this point, and approving just lets the check confirm (correctly) that there's nothing left to do. Safe to leave unapproved indefinitely if the check isn't a required status for merging.
</Note>

### Database enum sidebarTitle auto-fix

Only triggers on a PR touching `en/database/tables/enums/**`, a tree wholly generated by an external ADO pipeline (see #192) with no source file in this repo. Every page's `title` follows one fixed shape, `Enum values for <Name>`, which otherwise renders unabbreviated in the sidebar. Unlike every other check on this page, this one doesn't just warn or fail - it derives a short `sidebarTitle` from `<Name>`, and if any changed page in the PR is missing it, pushes a fix commit directly onto the PR branch. Only files matching the exact `Enum values for <Name>` shape are touched; anything else is left alone rather than guessed at. Because the fix is idempotent (a page that already has `sidebarTitle` is left untouched), the fix commit's own push re-triggers this same check but produces no further commit - it self-terminates rather than looping. Skipped entirely for PRs from a fork, since the default token can't push back to a fork's branch.

### MDO-provider title/sidebarTitle auto-fix

Only triggers on a PR touching `en/api/mdo-providers/reference/**`, a tree wholly generated by an external ADO pipeline (see #192) with no source file in this repo. Most page titles are short, but 36 of 421 (at the time of writing) share one structured shape - `relationssearchlistprovider<entity><entity?><active|passive>`, from a closed 5-entity vocabulary (appointment, contact, document, person, sale). Unrecased, that shape reads poorly as a page title and, unabbreviated, renders as one very long unbroken string in the sidebar - two separate problems, fixed in two separate fields.

This check PascalCases every recognized word and rewrites `title` in full (e.g. `relationssearchlistproviderappointmentappointmentpassive` -> `RelationsSearchListProviderAppointmentAppointmentPassive`, preserving every part of the name), then separately derives a short `sidebarTitle` that drops the shared `relationssearchlistprovider` prefix - redundant information once a page is already inside this tree in the sidebar - and space-joins the rest (-> "Appointment Appointment Passive"). Like the other auto-fix checks on this page, it pushes a fix commit directly onto the PR branch if any changed page is missing either field, rather than just warning or failing.

Only the confirmed `relationssearchlistprovider*` shape is touched - a handful of other long, irregular one-off names in this same tree (e.g. `ticketselectionwithstaticmenu`) don't share this pattern and are deliberately left alone rather than guessed at. Idempotent (a page that already has both fields set is left untouched, so the fix commit's own push produces no further commit), and skipped entirely for PRs from a fork, since the default token can't push back to a fork's branch.

### Reserved version frontmatter auto-fix

Only triggers on a PR touching `.md`/`.mdx` files (repo-wide, apart from a short list of excluded generated/reference paths - see the workflow file for the current list). Mintlify's search back end treats `version` as a reserved search-filter field and 400s on the non-string value our own `version` property produced - so the property was renamed to `so_version` repo-wide (see [issue #254][9]). Like the two auto-fix checks above, this doesn't just warn - it rewrites `version:` to `so_version:` directly in any changed file's frontmatter and pushes a fix commit onto the PR branch, so an external pipeline that reintroduces the old key gets corrected automatically rather than needing a human to notice a warning and fix it by hand. The rename is a pure key swap - the value and every other frontmatter line are left untouched. Idempotent (a file that already uses `so_version:` is left alone, so the fix commit's own push produces no further commit), and skipped entirely for PRs from a fork, since the default token can't push back to a fork's branch.

### Reference-page overflow auto-fix

Triggers on a PR touching either maintained list (`tools/ci/wide-mode-pages.txt`, `tools/ci/code-wrap-pages.txt`) or the three generated reference trees they can name pages from (`en/database/tables/`, `en/api/archive-providers/reference/`, `en/api/mdo-providers/reference/`). Generated tables and code samples in these trees sometimes render wider than the content column and visually spill into the right TOC panel - not reliably predictable per page from the outside, so this doesn't try to classify every page automatically. Instead, two small human-curated lists name the pages that need a fix, and this check re-applies the fix to every listed page on every run (not just this PR's changed files), so a future ADO regen that drops the fix gets it re-added automatically:

* `wide-mode-pages.txt` - pages that get Mintlify's `mode: "wide"` frontmatter (hides the right TOC, keeps the left sidebar). An existing, different `mode:` value on a listed page is left untouched and reported as a conflict rather than overwritten.
* `code-wrap-pages.txt` - pages whose fenced code blocks get wrapped in a `<div className="wrap-code-samples">` marker, which `style.css` scopes a `white-space: pre-wrap` override to. Used instead of `mode: "wide"` where losing the TOC was judged worse than a wrapped code line; deliberately not a site-wide CSS change, since most multi-line indented code elsewhere in the docs relies on the default `white-space: pre` to avoid unexpected mid-line breaks.

Append a page path to either list (no extension - the tool resolves `.mdx`/`.md` itself, preferring `.mdx` if both exist) whenever a new overflowing page is found or reported. Both fixes are idempotent, and both are skipped entirely for PRs from a fork, since the default token can't push back to a fork's branch.

### OpenAPI conversion auto-fix

Only triggers on a PR touching `api-sources/openapi/agent/**` or `api-sources/openapi/rest/**` - hand-maintained Swagger 2.0 sources refreshed by an external NetServer/ADO build, not authored in this repo. Mintlify renders OpenAPI 3, so every source file has a converted counterpart under `openapi/agent/` or `openapi/rest/`, plus an entry in `config/nav-restful-agent-openapi.json` or `config/nav-restful-rest-openapi.json` wiring it into navigation - both are workaround output this repo generates and commits ahead of [issue #147][11]'s native OpenAPI 3 pipeline. Before this check existed, a source refresh had no mechanism forcing the derived output to be regenerated in the same PR, so it silently went stale (confirmed on [PR #232][12], which refreshed every source file but left `openapi/` and both nav files untouched for days).

For each PR, the check converts only the added/modified source files (not a full re-conversion of every spec) via `tools/convert-swagger-to-openapi.ps1`, removes the output for any deleted source file, then regenerates both nav files from the current `openapi/{agent,rest}/` directory listing so a newly added or removed spec is wired into (or dropped from) navigation without a manual edit. If any of that produces a real diff, it's pushed back onto the PR branch as a fix commit, same as the other auto-fix checks on this page.

The converter also works around one known upstream bug: two source files (`Swagger-v1-QuoteAgent.json`, `Swagger-v1-Selection.json`) `$ref` a schema definition that the NetServer Swagger generator omits from the same file, which otherwise makes conversion fail outright. The converter copies the missing definition in from a sibling file that still has it, against a throwaway in-memory copy - the tracked `api-sources/` file itself is never edited, since that folder is pipeline-owned.

This whole check, plus `tools/convert-swagger-to-openapi.ps1` itself, is a workaround for the lack of a native OpenAPI 3 pipeline output - see [issue #297][13]. Once #147 lands, delete this workflow and the converter script outright rather than trying to keep them in sync with a pipeline that no longer needs them; `tools/ci/generate-openapi-nav.py` is the one piece that may be worth keeping as a standing nav-drift check regardless of how the underlying specs get produced.

### Recent-pages sync auto-fix

Triggers on a PR touching `index.mdx` itself, or anything under `en/**`/`integrations/**`. The homepage's "What's new" list is driven by `index.mdx`'s own `recent_pages` frontmatter - see [Metadata properties][14] for the property's shape - and resolved into a separate `<RecentPages items={[...]} />` call in the page body, baked in as a literal prop rather than fetched at runtime.

Each run: detects brand-new pages this PR added under `en/`/`integrations/` only (not repo-wide - reference trees like `database/`/`automation/crmscript/reference/` regenerate too often to be a useful signal) and adds them; stamps a missing `since` with today's date, and refreshes a *pinned* entry's `since` if this PR touched that entry's own target page; drops any entry whose target is `generated: true`; drops non-pinned entries older than 120 days; caps the combined list at 7, with pinned entries getting guaranteed slots that still count against the cap. If any of that changes either the frontmatter or the body call, both are rewritten and pushed back onto the PR branch as a fix commit, same as the other auto-fix checks on this page. Idempotent (a page already in sync produces no further commit), and skipped entirely for PRs from a fork, since the default token can't push back to a fork's branch.

### Markdownlint

Lints only the `.md`/`.mdx` files changed in a PR (or pushed directly to `main`) using this repo's own `.markdownlint.yaml` rules. Results appear in the workflow run's log in the GitHub Actions tab - a flagged line does not block the merge, but is worth fixing before requesting review. See [markdownlint][3] for how to interpret and fix a specific rule violation, and how to run the same linter locally.

### Landing page template guard

Checks only the `.mdx` files changed in a PR (or pushed directly to `main`) for a `mode: "custom"` landing page that mixes the two custom-landing Blueprint templates' card-body markup: a `category-landing` page (`id="category-landing"`) containing a subcategory-style `linklist` block, or a `subcategory` page (`id="subcategory"`) containing a category-landing-style `concept-links` list. `style.css` only styles `.linklist` under `#subcategory`, so a mismatch like this renders with unstyled, browser-default markup instead of the intended layout, without tripping any other check, since the two templates are only inconsistent with each other, not individually invalid MDX. It never fails the check - a hit shows up as a warning annotation on the relevant line in the PR's Files Changed tab.

### Release-notes API structure guard

Only triggers on a PR touching `release-notes/*/api/**`. `release-notes/{major}/api/` uses a flat structure - generated files sit directly under `MAJOR/api/`, using their original generated filename, with no per-minor-version subfolder (see [Add or update release notes][10]). This check warns when a changed file sits one directory level deeper than that instead - `release-notes/{major}/api/{subfolder}/...`. Historical dotted-version trees (`release-notes/10.1/api/`, `release-notes/9.2/api/`, etc.) use their own, different, frozen structure and are excluded by design - the check's path match requires a digit-only major version, which those don't have. It never fails the check - a hit shows up as a warning annotation on the PR's Files Changed tab, naming the file.

### Vale

Prose-style linting (tone, terminology, formatting), configured entirely by this repo's `.vale.ini` - no separate setup needed once the add-on is toggled on in the Mintlify dashboard. Set to Warning level to match markdownlint's advisory-only posture. Results show on the PR's checks list from Mintlify's own GitHub App.

### Build validation - why there is no separate `mint validate` step

`mint validate` is a build/schema validator (`docs.json` schema, MDX parse errors, OpenAPI spec validation) - it is unrelated to Vale and does not check broken links or accessibility. Those are separate CLI commands (`mint broken-links`, `mint a11y`). It is not run as its own GitHub Action here because the PR preview deployment already performs the equivalent build validation on every PR, live, as part of generating the preview - adding a duplicate `mint validate` Action would just re-check what the preview build already checked. A failed build already surfaces as the red "Mintlify Deployment" check described in [Deployment][2].

### Broken links (per-PR)

Mintlify's Broken Links add-on scans the whole site's internal links (not external URLs) on every PR and reports results the same way as the Vale check above. It is set to Warning level for the same advisory-first reasoning as the other checks.

<Note>
  A Mintlify community report (October 2025) describes a case where a page serving a 404 still returned an HTTP 200 status, which could make this check under-report a real broken link. Do not treat "no broken links flagged" here as a guarantee - see the scheduled check and fallback tool below for a second opinion.
</Note>

### CRMScript reference drift

Only triggers on a PR touching `api-sources/crmscript/**`, the generated `en/automation/crmscript/reference/**`, or either generator script - not on every PR like the checks above. It regenerates the reference fresh from source and fails if the result does not match what is committed, catching both directions of drift with one mechanism: a source `.yml` edited without regenerating, and a generated `.mdx` hand-edited directly without touching its source. The failure message names the specific source file to check. See [Update the CRMScript reference][7] for the full mechanism and why this one is blocking rather than advisory - `api-sources/crmscript/*.yml` also feeds the CRMScript interpreter's own built-in intellisense, so a stale source is a product-facing risk, not just a docs one.

### Stale generated pages

Covers three content trees generated by a pipeline outside this repo: `en/api/archive-providers/reference/`, `en/api/mdo-providers/reference/`, and `en/database/tables/`. Each has a matching `nav-<tree>.json` that gets rewritten whenever new or changed pages are pushed. When a source item on the generator side is deleted or renamed, the push removes its entry from the nav file, but nothing else removes the now-unreferenced page from this repo - it stays as a live, unlinked page indexed by site search. Unlike the CRMScript check above, there is no source file in this repo to regenerate from, so this only checks one direction: any page on disk that is no longer referenced by its tree's nav file fails the check, naming the file. Pages not marked `generated: true` in frontmatter are never touched, even if they look stale - fix by either deleting the file (run `tools/ci/find-stale-generated-pages.py --delete` locally) or adding it back to the relevant `nav-*.json` if it turns out to be a real nav omission rather than an actual deletion or rename.

## Scheduled check: repo-wide broken-link detection

A per-PR link check only catches breaks introduced by that PR. A separate, Mintlify-hosted automation periodically re-scans the *entire* site, catching drift from any source - including direct admin-bypass pushes that skip PR review entirely.

**Active** since August 11, 2026, first run August 15, 2026 (`nextRunAt` confirmed via `mint automations list`).

* **Type:** `broken-link-detection`

* **Cadence:** `0 6 1,15 * *` - the 1st and 15th of each month, run at 06:00. Standard cron has no native "every 2 weeks" field; this is the closest non-drifting approximation of a biweekly cadence (actual gaps land at 13-16 days depending on the month).

* **Auto-merge:** off. When the automation finds and fixes a broken link, it opens a PR like any other change - it goes through the same manual review as everything else in this repo, it does not merge itself.

* **What it does:** for each broken internal link, it checks whether the target page was renamed or moved and updates the link, or - if the target was removed entirely - edits the surrounding copy to remove or replace the reference.

* **Cost:** billed in Mintlify credits per run (rough public estimate: \~285 credits/run) - a real operating cost, not a free background job.

Setup record (issue [#81][4]):

```sh theme={null}
mint automations create --name "Broken link detection" --type broken-link-detection --cron "0 6 1,15 * *" --format json
```

`mint automations list` confirms it registered with `status: active`, no `automerge` field (off by default), and the cron/type above.

## Fallback: manual broken-link check

`tools/triage-broken-links.py` (built during [#129][5]) plus the `mint broken-links` CLI remain the trusted manual method for a broken-link sweep, separating genuine breaks from known false positives. Reach for this:

* Right after a large structural change (a folder move, a nav overhaul) rather than waiting for the next scheduled automation run.
* As a second opinion if the scheduled automation or the per-PR add-on reports "no breaks found" but something looks off - see the HTTP-200-masking-a-404 caveat above.

## Related content

* [Mintlify deployment and CI/CD][1] - the deploy pipeline and PR preview mechanics these checks build on.
* [markdownlint][3] - rule details and local usage.
* [Reviewing a PR][6] - the review checklist that links back here.
* [Update the CRMScript reference][7] - the regeneration workflow the drift check enforces.
* [Acceptance testing][8] - the on-demand checklist to run after a refactor, design change, or site reorg, rather than every PR.

[1]: ./deployment

[2]: ./deployment#deploy-trigger-mechanics

[3]: ./markdown-guide/markdownlint

[4]: https://github.com/SuperOfficeDocs/docs/issues/81

[5]: https://github.com/SuperOfficeDocs/docs/issues/129

[6]: ./review/index

[7]: ./how-to-update-crmscript-reference

[8]: ./acceptance-testing

[9]: https://github.com/SuperOfficeDocs/docs/issues/254

[10]: ./how-to-add-release-notes

[11]: https://github.com/SuperOfficeDocs/docs/issues/147

[12]: https://github.com/SuperOfficeDocs/docs/pull/232

[13]: https://github.com/SuperOfficeDocs/docs/issues/297

[14]: ./markdown-guide/metadata


## Related topics

- [Mintlify deployment and CI/CD](/contribute/deployment.md)
- [Acceptance testing](/contribute/acceptance-testing.md)
- [markdownlint](/contribute/markdown-guide/markdownlint.md)
