From 82d9dbae8a3985bbd5181387ea0c293e8aa0355d Mon Sep 17 00:00:00 2001 From: "benjamin.baker" Date: Sat, 5 Sep 2026 17:50:02 -0400 Subject: [PATCH] Fix CI: untrack docs prose, correct two stale frontend specs wilm has been red for eight consecutive runs across backend, gates and frontend. Three failures, two unrelated causes, neither introduced by the endpoint agent work. docs/ prose (backend + gates): The docs gates detect a published checkout by the ABSENCE of docs/*.md, because docs/ becomes the wiki on that side. DEV-BUILD-TEST.md and PLUGIN-HOOKS.md were committed in unrelated changes, which made CI look like an internal checkout carrying almost no prose. test_there_are_docs_ to_check then failed on `assert 3 > 20`, test_the_map_itself_is_current failed on a generated PROJECT-MAP.md that is correctly untracked, and check-gates-ran.sh failed on the resulting skip - a skipped gate reports green and checks nothing, so it refuses to accept one. The existing .gitignore comment predicted this exactly and was defeated by naming files individually. Untracked both (kept on disk) and replaced the per-file list with docs/*.md so the next authored doc cannot reintroduce it. Scoped to the top level: docs/notes/ prose and docs/openapi.json stay tracked, and the gates only glob docs/*.md. Frontend specs: getEntraDeviceUrls() gained lapsUrl and loaded, but two siteSettings specs still asserted the old two-key shape with toEqual, which is exact-match. The implementation is correct - loaded is what lets a caller distinguish a failed fetch from an unconfigured site, since both read as empty strings - so the specs were brought up to the current shape rather than the return value narrowed. pinnedPanels.spec.js sliced 420 characters from getEntraDeviceUrls() and asserted the finally block appeared inside it. Adding the retry logic pushed finally past that offset, so a correct implementation reported as "the finally is missing". Rebounded the slice to the function's own extent, matching the structural approach already used elsewhere in this file for the same reason. Verified by mutation: renaming the flag in the source fails the test, which also caught that the spec reads plugins/computers/frontend/views/PCDetail.vue and not the .plugins-staged build copy. --- .gitignore | 30 +++++++------ docs/DEV-BUILD-TEST.md | 45 -------------------- docs/PLUGIN-HOOKS.md | 45 -------------------- frontend/src/components/pinnedPanels.spec.js | 9 +++- frontend/src/utils/siteSettings.spec.js | 8 +++- tests/test_docs_publishable.py | 18 +++++++- tests/test_docs_versions.py | 9 +++- 7 files changed, 56 insertions(+), 108 deletions(-) delete mode 100644 docs/DEV-BUILD-TEST.md delete mode 100644 docs/PLUGIN-HOOKS.md diff --git a/.gitignore b/.gitignore index 31591d72..20e29de4 100644 --- a/.gitignore +++ b/.gitignore @@ -91,20 +91,24 @@ _transfer/ # Local developer scratch notes and chat handoffs - never committed devnotes/ -# docs/ is generated/published to the wiki, not tracked in the product repo. -# A single tracked file here makes docs/ exist in the CI checkout, which -# defeats the intentional skip in the docs publish/version tests. Keep this -# local-only guide out of version control so those tests skip as designed. -docs/CSV-IMPORT-GUIDE.md +# docs/ prose is published to the wiki, not tracked in the product repo. The +# docs gates detect a published checkout by the ABSENCE of docs/*.md, so a +# single tracked prose file here makes CI look like an internal checkout while +# carrying almost no prose: test_there_are_docs_to_check then fails asserting +# >20 files, test_the_map_itself_is_current fails on a generated PROJECT-MAP +# that is correctly untracked, and check-gates-ran.sh fails on the resulting +# skip. That happened - DEV-BUILD-TEST.md and PLUGIN-HOOKS.md were committed in +# unrelated changes and broke the branch for days. Ignore the whole pattern +# rather than naming files one at a time, so the next authored doc cannot +# reintroduce it. +# +# Scoped to the top level on purpose: docs/notes/ prose stays tracked, and the +# gates only glob docs/*.md. docs/openapi.json is tracked (the app serves it) +# and is unaffected by a *.md rule. +docs/*.md -# Local-only authored docs (executive summaries, workflow/security writeups, -# planner exports, draft decks and diagrams). Same reasoning as above: keep -# personal docs out of version control. docs/openapi.json stays tracked and -# is intentionally NOT ignored here. -docs/EXECUTIVE-SUMMARY.md -docs/DESIGN-DECISIONS-agent.md -docs/OPERATOR-WORKFLOW.md -docs/SECURITY-RISK-ASSESSMENT.md +# Local-only authored docs (planner exports, draft decks and diagrams). Same +# reasoning as above: keep personal docs out of version control. docs/ShopDB-Planner-Tasks.csv docs/DRAFT--ShopDB-CSV-Import-Guide.pptx docs/diagrams/ diff --git a/docs/DEV-BUILD-TEST.md b/docs/DEV-BUILD-TEST.md deleted file mode 100644 index ac357d1b..00000000 --- a/docs/DEV-BUILD-TEST.md +++ /dev/null @@ -1,45 +0,0 @@ -# ShopDB dev build - quick test - -Two dev builds you can stand up on a Windows box with Docker Desktop. You run -one PowerShell script: it checks your dependencies (offers to winget-install -anything missing), then PULLS a prebuilt image from GHCR and starts the stack in -Docker. Nothing is compiled on your machine - no Node, no npm, no source build - -so it works on a box that has only Docker installed. - -## What you need - -- Windows with Docker Desktop running. -- Git for Windows (`winget install Git.Git`). -- A GitHub login for this private repo/registry already stored on the box. If - you have cloned this repo before, or signed in to GitHub in Visual Studio or - VS Code, you already have it - the script uses that stored credential - automatically. If Docker cannot pull the image, sign in once with - `docker login ghcr.io` (use a GitHub username and a personal access token that - has `read:packages`), then re-run the launcher. - -## Get the launcher - -You will be sent `Start-ShopDBDev.ps1` directly. Save it anywhere (for example -your Downloads folder) and open PowerShell in that folder. - -## Bleeding edge (dev-be) - - powershell -ExecutionPolicy Bypass -File .\Start-ShopDBDev.ps1 -Channel edge - -Comes up at https://localhost:5011. Red DEV badge in the sidebar. Pulls the -`edge` image. - -## Stable (main) - - powershell -ExecutionPolicy Bypass -File .\Start-ShopDBDev.ps1 -Channel stable - -Comes up at https://localhost:5001. No DEV badge. Pulls the `stable` image. - -## Notes - -- Both stacks can run at the same time - they use different ports and separate - Docker volumes, so they do not collide and each keeps its own database. -- Each launch pulls the latest image for the channel, so re-running the launcher - is how you pick up a new build. No local rebuild step. -- First page load shows a self-signed cert warning (expected for a dev box). -- Stop a stack: `docker compose -p shopdb-stable down` (or `shopdb-edge`). diff --git a/docs/PLUGIN-HOOKS.md b/docs/PLUGIN-HOOKS.md deleted file mode 100644 index a4b3f531..00000000 --- a/docs/PLUGIN-HOOKS.md +++ /dev/null @@ -1,45 +0,0 @@ -# Plugin hooks and contract surface - -Everything a plugin may import from the core lives in `shopdb.api` (ADR-001), -versioned by `__contract_version__` (ADR-002). Adding a name is a minor bump; -removing one is major. This file records the non-obvious hooks - the ones a -plugin author would not guess from the name alone. It is not the full export -list; read `shopdb/api/__init__.py` for that. - -## Reversible bulk-import journal - -Contract version 0.30.0. - -A plugin that drives a bulk import - a site asset adoption, an integration sync - -records the primitive changes it makes so the whole batch can be reversed later. -The journal is import-agnostic: it stores primitive mutations (set a cell, insert -a row) tied to a batch id, and a generic undo replays them in reverse. Nothing in -core learns what the import means. - -Exposed on `shopdb.api`: - -- `ImportBatch` - one row per applied import. `batchid` is the identity that the - approval step, the write, and the undo all reference. `status` is `applied` or - `undone`. -- `ImportJournalEntry` - one reversible primitive mutation. `operation` is - `update` (a cell change, carrying `oldvalue`/`newvalue` so it can be put back) - or `insert` (a row this batch created, so undo can delete strictly what the - import created). -- `BATCH_APPLIED`, `BATCH_UNDONE` - the two `ImportBatch.status` values. -- `OP_UPDATE`, `OP_INSERT` - the two `ImportJournalEntry.operation` values. - -What a plugin gets from this, and what it does NOT: - -- A plugin's import UI reads `ImportBatch` to list applied batches (kind, row - count, who applied it, when) and offers an "Undo import" per batch. -- A plugin does NOT write the journal or run undo through these model imports. - The write/undo logic lives with the import writer (see - `scripts/site_imports/journal.py`), which records entries in the SAME - transaction as the mutation they describe and reverses them conservatively: - an `update` is put back only if the cell still holds the value the import - wrote (a value a human changed since is left as-is and reported), and an - `insert` is hard-deleted only when nothing outside the batch still references - the created row. - -The tables are core-owned (migration `7d37_import_batches_and_journal`), so they -exist regardless of which plugins are installed. diff --git a/frontend/src/components/pinnedPanels.spec.js b/frontend/src/components/pinnedPanels.spec.js index f5ac0499..bf1ee53c 100644 --- a/frontend/src/components/pinnedPanels.spec.js +++ b/frontend/src/components/pinnedPanels.spec.js @@ -88,9 +88,14 @@ describe('the header cloud identity block', () => { it('marks the templates settled even when the fetch fails', () => { // Otherwise a down settings backend hides the block permanently. + // Bounded by the function's own closing brace rather than a character + // count: a fixed window silently excluded the finally block as soon as + // the retry logic grew, which reported as "the finally is missing" when + // the real code was correct. Slice to the next top-level declaration. const source = pcDetailSource() - const start = source.indexOf('getEntraDeviceUrls()') - const block = source.slice(start, start + 420) + const start = source.indexOf('async function loadPortalTemplates') + const next = source.indexOf('\nonUnmounted(', start) + const block = source.slice(start, next === -1 ? undefined : next) expect(block).toContain('finally') expect(block).toContain('portalTemplatesLoaded.value = true') }) diff --git a/frontend/src/utils/siteSettings.spec.js b/frontend/src/utils/siteSettings.spec.js index 7174329d..bb5fa838 100644 --- a/frontend/src/utils/siteSettings.spec.js +++ b/frontend/src/utils/siteSettings.spec.js @@ -70,6 +70,8 @@ describe('loading', () => { expect(await settings.getEntraDeviceUrls()).toEqual({ entraUrl: 'https://entra/{id}', intuneUrl: 'https://intune/{id}', + lapsUrl: '', + loaded: true, }) expect(list).toHaveBeenCalledTimes(1) }) @@ -82,7 +84,11 @@ describe('failure', () => { const settings = await freshModule() expect(await settings.getFacilityName()).toBe('ShopDB') - expect(await settings.getEntraDeviceUrls()).toEqual({ entraUrl: '', intuneUrl: '' }) + // loaded:false is the point - a failed fetch and an unconfigured site both + // read as empty strings, and the caller needs to tell them apart. + expect(await settings.getEntraDeviceUrls()).toEqual({ + entraUrl: '', intuneUrl: '', lapsUrl: '', loaded: false, + }) }) it('does not hammer a backend that is down', async () => { diff --git a/tests/test_docs_publishable.py b/tests/test_docs_publishable.py index 2598a99f..c4256873 100644 --- a/tests/test_docs_publishable.py +++ b/tests/test_docs_publishable.py @@ -94,7 +94,23 @@ def documentation_files(): def test_there_are_docs_to_check(): """A path change that silently matched nothing would make this suite pass - while checking absolutely nothing.""" + while checking absolutely nothing. + + FLAG FOR DOC AUTHORS: if this fails with a SMALL number (3, not 0), you have + almost certainly just committed a docs/*.md file. Don't lower the threshold + to make it pass - that is the exact failure this assertion exists to catch, + and it would silently disable the whole scrub suite. + + docs/*.md prose is wiki-published and MUST stay untracked. The gates decide + 'is this a published checkout?' by the ABSENCE of docs/*.md, so one tracked + prose file makes CI look internal while carrying no prose, and takes down + backend AND gates together. docs/DEV-BUILD-TEST.md and docs/PLUGIN-HOOKS.md + did exactly this and held the branch red for eight runs; both were untracked + (kept on disk) and .gitignore now ignores docs/*.md wholesale. + + Authoring a doc is still fine - write it in docs/, it just stays local and + goes to the wiki. docs/notes/ and docs/openapi.json are unaffected. + """ assert len(documentation_files()) > 20 diff --git a/tests/test_docs_versions.py b/tests/test_docs_versions.py index 4b001c01..e743eac8 100644 --- a/tests/test_docs_versions.py +++ b/tests/test_docs_versions.py @@ -121,7 +121,14 @@ def test_a_stated_plugin_count_matches_the_tree(): def test_the_map_itself_is_current(): """The pointer target has to be right, or every document pointing at it is - wrong at one remove.""" + wrong at one remove. + + FLAG FOR DOC AUTHORS: if this fails saying PROJECT-MAP.md is missing, the + likely cause is not a missing map - it is that you committed a docs/*.md + file, which stopped this module from skipping as a published checkout. The + map is generated and correctly untracked. See the note in + tests/test_docs_publishable.py::test_there_are_docs_to_check. + """ mapfile = DOCS / 'PROJECT-MAP.md' assert mapfile.is_file(), 'the PROJECT-MAP wiki page is missing; run scripts/gen_project_map.py' text = mapfile.read_text()