From a540feb8b61a9248e73f045346d7120d57419754 Mon Sep 17 00:00:00 2001 From: Carson Davis Date: Wed, 17 Jun 2026 11:36:34 -0500 Subject: [PATCH 01/13] Run the test suite against both deployment modes (full and lean) in CI (#151) Add a [full, lean] deployment-mode matrix to the e2e/boot CI leg and the checks that make a mode-specific break fail CI before merge: - Split CI into a single hermetic unit job (vitest) and a matrixed e2e job; surface the mode in job and artifact names. - New tests/e2e/deployment-mode.spec.js: a hand-written feature inventory asserting each full-only feature is reachable in full and gone in lean, and the lean-only dashboard publish flow the reverse. One loop covers both directions. The gating step has no continue-on-error, so a mis-gated feature turns the leg red (acceptance #5). - New tests/ci/assert-gated-tables.js: gated-off features still have their DB tables in each mode (model sync is unconditional; only route mounts are gated), so a later mode flip needs no data migration (acceptance #4). - Lean leg disables the sidecar WITH_* flags so init-db and boot succeed without the services lean doesn't deploy (acceptance #3). - Short two-mode contributor note in README and AGENTS (author in full first, then confirm lean). No application/runtime source changes. Verified locally: both legs' spec passes 8/8, lean boots sidecar-free, and the gated tables exist in lean. --- .github/workflows/playwright-tests.yml | 87 ++++++++++++-- AGENTS.md | 9 ++ README.md | 11 ++ tests/ci/assert-gated-tables.js | 95 ++++++++++++++++ tests/e2e/deployment-mode.spec.js | 151 +++++++++++++++++++++++++ 5 files changed, 343 insertions(+), 10 deletions(-) create mode 100644 tests/ci/assert-gated-tables.js create mode 100644 tests/e2e/deployment-mode.spec.js diff --git a/.github/workflows/playwright-tests.yml b/.github/workflows/playwright-tests.yml index ad59c3429..d031fc8f4 100644 --- a/.github/workflows/playwright-tests.yml +++ b/.github/workflows/playwright-tests.yml @@ -7,9 +7,43 @@ on: branches: [master, main, development] jobs: + # Unit tests run once, outside the deployment-mode matrix. They are hermetic + # (Vitest + jsdom, no DB/browser) and already cover BOTH modes within a single + # run by busting the require cache per-test (see tests/unit/deploymentMode.spec.js + # and bakeGuards.spec.js). Matrixing them would just waste CI time. + unit: + timeout-minutes: 10 + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "20" + cache: "npm" + + - name: Install dependencies + run: npm ci + + - name: Run Unit Tests + run: npx vitest run + + # The e2e / app-boot leg runs once per deployment shape. The mode is resolved + # once at process start (deploymentMode.js), so each shape needs its own boot — + # hence the matrix. MMGIS_DEPLOYMENT_MODE is written into .env in the setup + # step below and read by both the server and the present/absent e2e test. test: + name: e2e (${{ matrix.mode }}) timeout-minutes: 20 runs-on: ubuntu-latest + needs: unit + + strategy: + fail-fast: false + matrix: + mode: [full, lean] services: postgres: @@ -39,12 +73,6 @@ jobs: - name: Install dependencies run: npm ci - # Unit tests run under Vitest in a jsdom environment — hermetic, so they - # need neither the database nor a browser. Run them before the e2e-only - # setup so a DB/browser problem can't mask a unit result. - - name: Run Unit Tests - run: npx vitest run - - name: Install Playwright Browsers run: npx playwright install --with-deps chromium @@ -62,19 +90,58 @@ jobs: echo "ENABLE_MMGIS_WEBSOCKETS=false" >> .env echo "ENABLE_CONFIG_WEBSOCKETS=false" >> .env echo "HIDE_CONFIG=true" >> .env + echo "MMGIS_DEPLOYMENT_MODE=${{ matrix.mode }}" >> .env + + # The lean leg must boot WITHOUT the sidecar services it doesn't deploy. + # sample.env ships several WITH_* sidecars on (TIPG/TITILER/TITILER_PGSTAC/ + # VELOSERVER); leaving them on in lean would have init-db try to create the + # mmgis-stac catalog DB and run pypgstac — neither exists in the lean + # topology. deploymentMode/init-db already gate the STAC DB on isFull(), + # but we also turn the WITH_* flags off here so the lean leg's env honestly + # reflects a lean deployment (no sidecars, no spatial-catalog DB). + - name: Disable sidecar services for the lean leg + if: matrix.mode == 'lean' + run: | + echo "WITH_STAC=false" >> .env + echo "WITH_TIPG=false" >> .env + echo "WITH_TITILER=false" >> .env + echo "WITH_TITILER_PGSTAC=false" >> .env + echo "WITH_VELOSERVER=false" >> .env + # init-db must succeed under the running mode. In lean this also proves the + # spatial-catalog (mmgis-stac) DB is NOT created (it's gated on isFull()). - name: Initialize Database run: node scripts/init-db.js - - name: Run E2E Tests - run: npx playwright test tests/e2e --project=chromium + # Acceptance #4: features gated OFF in lean must still have their DB tables + # (model sync runs unconditionally; only route mounts are gated), so a + # later mode flip needs no data migration. This step boots the app once + # (which runs sequelize.sync()) and then asserts the gated tables exist. + # No continue-on-error: a missing table fails the leg. + - name: Assert gated tables exist + run: node tests/ci/assert-gated-tables.js + + # Legacy smoke / event-bus e2e tests. These boot the full SPA and have + # historically been allowed to not block the build (continue-on-error), + # so a genuinely-flaky legacy assertion doesn't turn the leg red. Kept + # non-gating here to preserve that behavior; the gating happens in the + # mode-shape step below. + - name: Run legacy E2E Tests (non-gating) + run: npx playwright test tests/e2e/smoke.spec.js tests/e2e/eventbus-integration.spec.js --project=chromium continue-on-error: true + # Acceptance #1 + #5: the deployment-mode present/absent check MUST fail + # CI when a shape breaks. NO continue-on-error here — a mis-gated feature + # (wrongly on in lean, or wrongly off in full) turns this leg red. This is + # the step that makes "a breaking change to either shape fails CI" real. + - name: Run deployment-mode shape check (gating) + run: npx playwright test tests/e2e/deployment-mode.spec.js --project=chromium + - name: Upload test results if: always() uses: actions/upload-artifact@v4 with: - name: playwright-report + name: playwright-report-${{ matrix.mode }} path: playwright-report/ retention-days: 30 @@ -82,6 +149,6 @@ jobs: if: always() uses: actions/upload-artifact@v4 with: - name: test-results + name: test-results-${{ matrix.mode }} path: test-results/ retention-days: 7 diff --git a/AGENTS.md b/AGENTS.md index 360ab55e4..3e47232ab 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -9,6 +9,15 @@ Use MCP tools when possible for code analysis, symbol navigation, and code modifications. Local development uses hot-reloading and therefore there is little reason to run `npm run build` for the user. +## Deployment modes (two shapes from one codebase) + +MMGIS deploys in two shapes selected by `MMGIS_DEPLOYMENT_MODE` (resolved once at startup in `API/Backend/Utils/deploymentMode.js`): + +- **`full`** (default) — the complete application as shipped today. +- **`lean`** — a gated-down deployment: geodata management, the bundled sidecar services/proxy, the on-disk mission filesystem, the link shortener, and server-side raster utilities are turned OFF; the dashboard publish flow (Deployments) is turned ON. Models still sync in both modes, so the gated-off tables exist regardless of mode (no migration needed to flip modes). + +Contributor rule: **author your change for the full app first, then confirm lean still passes.** CI runs the e2e/boot suite once per mode (`.github/workflows/playwright-tests.yml`), with a present/absent check (`tests/e2e/deployment-mode.spec.js`) whose expected on/off mapping is hand-written from the feature inventory — keep it independent of any capability table. + ## Project Overview **MMGIS** is a web-based mapping and localization solution for science operations on planetary missions, developed by NASA-AMMOS. It provides spatial data infrastructure for mission-critical geospatial visualization and collaboration, supporting both 2D (Leaflet) and 3D (Cesium) mapping with real-time multi-user collaboration. diff --git a/README.md b/README.md index 4b1b66e2d..778a78632 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,17 @@ --- +## Deployment modes + +MMGIS builds from one codebase into two deployment shapes, selected by the `MMGIS_DEPLOYMENT_MODE` environment variable: + +- **`full`** (default) — the complete MMGIS application as shipped today. Used when the variable is unset. +- **`lean`** — a lighter, smaller-footprint deployment that deliberately turns a set of server features off (geodata management, the bundled sidecar services/proxy, the on-disk mission filesystem, the link shortener, server-side raster utilities) and turns on the dashboard publish flow. Database tables for the gated-off features are still created, so switching a deployment from one mode to the other needs no data migration. + +CI runs the end-to-end / boot suite once per mode. Contributors should author changes for the full app first, then confirm lean still passes. + +--- + ## Installation --- diff --git a/tests/ci/assert-gated-tables.js b/tests/ci/assert-gated-tables.js new file mode 100644 index 000000000..35f56fefb --- /dev/null +++ b/tests/ci/assert-gated-tables.js @@ -0,0 +1,95 @@ +/** + * assert-gated-tables.js + * + * Acceptance #4 of the both-modes CI coverage: features that are gated OFF in a + * given deployment mode must STILL have their database tables created, so a + * later mode flip needs no data migration. Model registration + sequelize.sync() + * run unconditionally on boot — only the route MOUNTS are gated — so the tables + * should exist regardless of mode. + * + * This script mirrors what scripts/server.js does at boot WITHOUT starting the + * HTTP server (and therefore without a webpack build): it loads every backend + * setup module (each of which requires its Sequelize models, registering them on + * the shared connection), runs sequelize.sync() to create the tables, then + * asserts the gated-feature tables are present. + * + * ORDERING: sequelize.sync() creates tables with PostGIS geometry columns (e.g. + * user_features), so the postgis extension must already exist. The CI workflow + * runs scripts/init-db.js (which enables postgis) BEFORE this script. Do not run + * it standalone against a bare database or sync() throws + * `type "geometry" does not exist`. + * + * It runs in BOTH legs of the CI matrix. In lean, the gated-OFF features + * (datasets, geodatasets, draw/user-files, the link shortener) have no mounted + * routes — this proves their tables exist anyway. In full, the lean-only + * Deployments feature is the gated-OFF one — its table must exist too. Rather + * than special-case per mode, we assert the union: every table below must exist + * in every mode. + * + * Exits non-zero (failing the CI leg) if any expected table is missing. + */ + +require("dotenv").config({ path: __dirname + "/../../.env" }); + +const { MODE } = require("../../API/Backend/Utils/deploymentMode"); +const setups = require("../../API/setups"); +const { sequelize } = require("../../API/connection"); + +// Hand-written from the deployment feature inventory: the tables behind the +// features that are gated OFF in one mode or the other. They must exist in BOTH +// modes. The shortener model is `url_shortener`; Sequelize pluralizes it to +// `url_shorteners` by default, so accept either spelling. +const REQUIRED_TABLE_GROUPS = [ + ["datasets"], // geodata management (datasets) + ["geodatasets"], // geodata management (geodatasets) + ["user_files"], // on-disk mission filesystem / drawing + ["user_features"], // drawing (vector features) + ["url_shorteners", "url_shortener"], // link shortener + ["deployments"], // lean-only dashboard publish flow +]; + +async function main() { + await new Promise((resolve) => { + // Loading the backend setups requires each feature's setup.js, which in + // turn requires its models — registering them on the shared sequelize. + setups.getBackendSetups(() => resolve()); + }); + + await sequelize.authenticate(); + await sequelize.sync(); + + const tables = await sequelize + .getQueryInterface() + .showAllTables(); + const present = new Set(tables.map((t) => String(t).toLowerCase())); + + const missing = []; + for (const group of REQUIRED_TABLE_GROUPS) { + const found = group.some((name) => present.has(name.toLowerCase())); + if (!found) missing.push(group.join(" | ")); + } + + if (missing.length > 0) { + console.error( + `[assert-gated-tables] MODE=${MODE}: missing expected table(s): ${missing.join( + ", " + )}` + ); + console.error( + `[assert-gated-tables] tables present: ${[...present] + .sort() + .join(", ")}` + ); + process.exit(1); + } + + console.log( + `[assert-gated-tables] MODE=${MODE}: all ${REQUIRED_TABLE_GROUPS.length} gated-feature table groups present.` + ); + process.exit(0); +} + +main().catch((err) => { + console.error("[assert-gated-tables] Unexpected failure:", err); + process.exit(1); +}); diff --git a/tests/e2e/deployment-mode.spec.js b/tests/e2e/deployment-mode.spec.js new file mode 100644 index 000000000..5d0d432f3 --- /dev/null +++ b/tests/e2e/deployment-mode.spec.js @@ -0,0 +1,151 @@ +import { test, expect, request } from '@playwright/test' + +/** + * Deployment-mode present/absent checks (acceptance #2 of the both-modes CI + * coverage issue). + * + * The same codebase ships in two shapes selected by MMGIS_DEPLOYMENT_MODE: + * - full: the complete application as shipped today. + * - lean: a gated-down deployment that turns a set of server features OFF. + * + * The CI matrix boots the app once per shape and passes the mode in via + * MMGIS_DEPLOYMENT_MODE; this test reads that and, per feature, asserts the + * feature is reachable when it belongs to the running mode and gone otherwise. + * + * HAND-WRITTEN feature inventory — IMPORTANT: the expected on/off mapping below + * is written by a person from the deployment ADR feature inventory, NOT read + * from any capability table or from the gated code. That independence is the + * whole point: a test that takes its expected answers from the thing it is + * testing cannot catch a wrong entry. The route PATHS were looked up in the + * code, but which mode each belongs to is hand-asserted here. Do NOT later + * rewrite this to read its expectations from a capability table. + * + * How a route reports present vs. absent (verified against the code): + * - A MOUNTED route answers from its own handler. With AUTH=off these handlers + * return HTTP 200 with a JSON failure body (auth/guard rejection) or real + * data, and the sidecar proxy answers with a proxy/upstream error — in every + * case the status is NOT 404. + * - An UNMOUNTED route falls through to the app's catch-all + * (`app.all('*')` -> res.status(404).render('error')), i.e. a real HTTP 404. + * So the reliable discriminator is: mounted => status !== 404; absent => 404. + */ + +const MODE = process.env.MMGIS_DEPLOYMENT_MODE || 'full' + +// Each feature: where it belongs, and a single HTTP probe that exercises a route +// the feature owns. `method`/`path` only — no body needed, because we only care +// whether the route is mounted (any non-404) or absent (404). +const FEATURES = [ + // --- Full-only: present in full, absent in lean --- + { + name: 'geodata management (datasets)', + mode: 'full', + method: 'post', + // ensureAdmin allow-lists /api/datasets/get, so in full it reaches the + // mounted router even without admin auth. + path: '/api/datasets/get', + }, + { + name: 'geodata management (geodatasets)', + mode: 'full', + method: 'get', + // ensureAdmin allow-lists /api/geodatasets/get. + path: '/api/geodatasets/get', + }, + { + name: 'drawing (draw API)', + mode: 'full', + method: 'post', + path: '/api/draw/add', + }, + { + name: 'on-disk mission filesystem (files API)', + mode: 'full', + method: 'post', + path: '/api/files/getfiles', + }, + { + name: 'link shortener', + mode: 'full', + method: 'post', + path: '/api/shortener/shorten', + }, + { + name: 'server-side raster utilities', + mode: 'full', + method: 'post', + // /api/utils is mounted in both modes, but the raster endpoints inside + // it (getbands/getprofile/...) are registered only when isFull(). + path: '/api/utils/getbands', + }, + { + name: 'bundled sidecar services / proxy (titiler)', + mode: 'full', + method: 'get', + // The /titiler proxy is mounted only when isFull() AND WITH_TITILER=true + // (the full CI leg keeps the sample.env sidecar flags on). The lean leg + // turns the WITH_* flags off and the proxy is also isFull()-gated, so it + // is absent there. + path: '/titiler/healthz', + }, + + // --- Lean-only: present in lean, absent in full --- + { + name: 'dashboard publish flow (deployments)', + mode: 'lean', + method: 'get', + // Mounted only when isLean(); ensureAdmin rejects with a 200 JSON body + // (not 404) when present. + path: '/api/deployments', + }, +] + +async function probe(api, feature) { + const res = + feature.method === 'post' + ? await api.post(feature.path, { data: {} }) + : await api.get(feature.path) + return res.status() +} + +test.describe(`Deployment mode present/absent — MODE=${MODE}`, () => { + let api + + test.beforeAll(async () => { + api = await request.newContext({ + baseURL: process.env.TEST_BASE_URL || 'http://localhost:8888', + ignoreHTTPSErrors: true, + }) + }) + + test.afterAll(async () => { + await api.dispose() + }) + + for (const feature of FEATURES) { + const belongsToRunningMode = feature.mode === MODE + + test(`${feature.name} is ${ + belongsToRunningMode ? 'present' : 'absent' + } in ${MODE}`, async () => { + const status = await probe(api, feature) + + if (belongsToRunningMode) { + // Reachable: the route is mounted, so it must NOT hit the + // catch-all 404. (It may legitimately answer 200 with a guard + // failure, real data, or a proxy/upstream error status.) + expect( + status, + `${feature.name} should be MOUNTED in ${MODE} (got ${status}); a 404 means the route is gone` + ).not.toBe(404) + } else { + // Gone: the route is unmounted and falls through to the + // app catch-all, which returns a real 404. + expect( + status, + `${feature.name} should be ABSENT in ${MODE} (got ${status}); anything but 404 means the route is still mounted` + ).toBe(404) + } + }) + } +}) From 281f5a9f66fe596530e6f58c70b813a09bd1ecf1 Mon Sep 17 00:00:00 2001 From: Carson Davis Date: Mon, 29 Jun 2026 15:31:30 -0500 Subject: [PATCH 02/13] Drop dangling acceptance-criteria cross-refs from CI comments The 'Acceptance #N' prefixes pointed at a numbered list that lives nowhere in the repo and read like GitHub issue links. The comments already explain their steps inline, so the prefixes only added confusion. Also trim a redundant restated-step-name sentence. --- .github/workflows/playwright-tests.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/playwright-tests.yml b/.github/workflows/playwright-tests.yml index d031fc8f4..c83835ca0 100644 --- a/.github/workflows/playwright-tests.yml +++ b/.github/workflows/playwright-tests.yml @@ -108,12 +108,12 @@ jobs: echo "WITH_TITILER_PGSTAC=false" >> .env echo "WITH_VELOSERVER=false" >> .env - # init-db must succeed under the running mode. In lean this also proves the - # spatial-catalog (mmgis-stac) DB is NOT created (it's gated on isFull()). + # In lean, this step also proves the spatial-catalog (mmgis-stac) DB is + # NOT created (it's gated on isFull()). - name: Initialize Database run: node scripts/init-db.js - # Acceptance #4: features gated OFF in lean must still have their DB tables + # Features gated OFF in lean must still have their DB tables # (model sync runs unconditionally; only route mounts are gated), so a # later mode flip needs no data migration. This step boots the app once # (which runs sequelize.sync()) and then asserts the gated tables exist. @@ -130,7 +130,7 @@ jobs: run: npx playwright test tests/e2e/smoke.spec.js tests/e2e/eventbus-integration.spec.js --project=chromium continue-on-error: true - # Acceptance #1 + #5: the deployment-mode present/absent check MUST fail + # The deployment-mode present/absent check MUST fail # CI when a shape breaks. NO continue-on-error here — a mis-gated feature # (wrongly on in lean, or wrongly off in full) turns this leg red. This is # the step that makes "a breaking change to either shape fails CI" real. From 66d41b455564dcfb9317ba3f907a3d21e7b80e8e Mon Sep 17 00:00:00 2001 From: Carson Davis Date: Mon, 29 Jun 2026 15:53:54 -0500 Subject: [PATCH 03/13] Trim the assert-gated-tables docblock down to the two real gotchas MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cut the 30-line header to ~14: drop the dangling 'Acceptance #4' cross-ref, the both-legs/union paragraph (already explained inline at the table list), and the obvious exit-code line. Keep only what a reader can't infer from the code — why gated-off features still need tables, and the postgis-before-sync ordering trap. --- tests/ci/assert-gated-tables.js | 31 ++++++++----------------------- 1 file changed, 8 insertions(+), 23 deletions(-) diff --git a/tests/ci/assert-gated-tables.js b/tests/ci/assert-gated-tables.js index 35f56fefb..230b23e1a 100644 --- a/tests/ci/assert-gated-tables.js +++ b/tests/ci/assert-gated-tables.js @@ -1,32 +1,17 @@ /** * assert-gated-tables.js * - * Acceptance #4 of the both-modes CI coverage: features that are gated OFF in a - * given deployment mode must STILL have their database tables created, so a - * later mode flip needs no data migration. Model registration + sequelize.sync() - * run unconditionally on boot — only the route MOUNTS are gated — so the tables - * should exist regardless of mode. + * Guards a both-modes invariant: a feature gated OFF in the current deployment + * mode must still have its DB tables, so flipping modes later needs no data + * migration. Model registration + sequelize.sync() run unconditionally on boot + * (only route mounts are gated), so the tables should exist in either mode. * - * This script mirrors what scripts/server.js does at boot WITHOUT starting the - * HTTP server (and therefore without a webpack build): it loads every backend - * setup module (each of which requires its Sequelize models, registering them on - * the shared connection), runs sequelize.sync() to create the tables, then - * asserts the gated-feature tables are present. + * Boots the backend setups (registering the models), runs sync(), and asserts + * the gated-feature tables are present — exits non-zero if any are missing. * - * ORDERING: sequelize.sync() creates tables with PostGIS geometry columns (e.g. - * user_features), so the postgis extension must already exist. The CI workflow - * runs scripts/init-db.js (which enables postgis) BEFORE this script. Do not run - * it standalone against a bare database or sync() throws + * Must run AFTER scripts/init-db.js: sync() creates PostGIS geometry columns, so + * the postgis extension has to exist first or it throws * `type "geometry" does not exist`. - * - * It runs in BOTH legs of the CI matrix. In lean, the gated-OFF features - * (datasets, geodatasets, draw/user-files, the link shortener) have no mounted - * routes — this proves their tables exist anyway. In full, the lean-only - * Deployments feature is the gated-OFF one — its table must exist too. Rather - * than special-case per mode, we assert the union: every table below must exist - * in every mode. - * - * Exits non-zero (failing the CI leg) if any expected table is missing. */ require("dotenv").config({ path: __dirname + "/../../.env" }); From 24bdc639465f38c2326231dedfb6b9cd645cfaf3 Mon Sep 17 00:00:00 2001 From: Carson Davis Date: Mon, 29 Jun 2026 15:56:38 -0500 Subject: [PATCH 04/13] Trim the deployment-mode spec header; drop the last acceptance cross-ref Cut the dangling 'acceptance #2' ref and the redundant full/lean re-definition (that lives in AGENTS.md). Kept the two parts that earn their space: the hand-written-inventory warning (independence from the capability table is what makes the test trustworthy) and the present-vs-absent 404 discriminator the assertions rely on. --- tests/e2e/deployment-mode.spec.js | 37 ++++++++++++------------------- 1 file changed, 14 insertions(+), 23 deletions(-) diff --git a/tests/e2e/deployment-mode.spec.js b/tests/e2e/deployment-mode.spec.js index 5d0d432f3..d78d172a7 100644 --- a/tests/e2e/deployment-mode.spec.js +++ b/tests/e2e/deployment-mode.spec.js @@ -1,33 +1,24 @@ import { test, expect, request } from '@playwright/test' /** - * Deployment-mode present/absent checks (acceptance #2 of the both-modes CI - * coverage issue). + * Deployment-mode present/absent checks. * - * The same codebase ships in two shapes selected by MMGIS_DEPLOYMENT_MODE: - * - full: the complete application as shipped today. - * - lean: a gated-down deployment that turns a set of server features OFF. - * - * The CI matrix boots the app once per shape and passes the mode in via - * MMGIS_DEPLOYMENT_MODE; this test reads that and, per feature, asserts the - * feature is reachable when it belongs to the running mode and gone otherwise. + * The CI matrix boots the app once per shape (MMGIS_DEPLOYMENT_MODE=full|lean); + * this test reads the mode and, per feature, asserts the feature is reachable + * when it belongs to the running mode and gone otherwise. * * HAND-WRITTEN feature inventory — IMPORTANT: the expected on/off mapping below - * is written by a person from the deployment ADR feature inventory, NOT read - * from any capability table or from the gated code. That independence is the - * whole point: a test that takes its expected answers from the thing it is - * testing cannot catch a wrong entry. The route PATHS were looked up in the - * code, but which mode each belongs to is hand-asserted here. Do NOT later - * rewrite this to read its expectations from a capability table. + * is written by a person from the deployment ADR, NOT read from any capability + * table or the gated code. That independence is the point: a test that takes its + * expected answers from the thing it tests can't catch a wrong entry. Route PATHS + * were looked up in the code, but which mode each belongs to is hand-asserted. + * Do NOT rewrite this to read its expectations from a capability table. * - * How a route reports present vs. absent (verified against the code): - * - A MOUNTED route answers from its own handler. With AUTH=off these handlers - * return HTTP 200 with a JSON failure body (auth/guard rejection) or real - * data, and the sidecar proxy answers with a proxy/upstream error — in every - * case the status is NOT 404. - * - An UNMOUNTED route falls through to the app's catch-all - * (`app.all('*')` -> res.status(404).render('error')), i.e. a real HTTP 404. - * So the reliable discriminator is: mounted => status !== 404; absent => 404. + * Present vs. absent discriminator (verified against the code): a MOUNTED route + * answers from its handler — with AUTH=off that's a 200 guard-failure/data body, + * or a proxy/upstream error for sidecars; never 404. An UNMOUNTED route falls + * through to the catch-all `app.all('*')` -> 404. So: mounted => status !== 404; + * absent => 404. */ const MODE = process.env.MMGIS_DEPLOYMENT_MODE || 'full' From ac0b0438bb8c41b1d5346634bb04901172212818 Mon Sep 17 00:00:00 2001 From: Carson Davis Date: Mon, 29 Jun 2026 16:55:45 -0500 Subject: [PATCH 05/13] Lead the assert-gated-tables docblock with the rule, not an abstract label 'Guards a both-modes invariant' announced a rule instead of stating it. Open with the concrete requirement (gated-off features keep their DB tables) so line 1 says the thing, no 'invariant' to decode first. --- tests/ci/assert-gated-tables.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/ci/assert-gated-tables.js b/tests/ci/assert-gated-tables.js index 230b23e1a..78bae9a8e 100644 --- a/tests/ci/assert-gated-tables.js +++ b/tests/ci/assert-gated-tables.js @@ -1,10 +1,10 @@ /** * assert-gated-tables.js * - * Guards a both-modes invariant: a feature gated OFF in the current deployment - * mode must still have its DB tables, so flipping modes later needs no data - * migration. Model registration + sequelize.sync() run unconditionally on boot - * (only route mounts are gated), so the tables should exist in either mode. + * A feature gated OFF in the current deployment mode must still have its DB + * tables, so flipping modes later needs no data migration. Model registration + + * sequelize.sync() run unconditionally on boot (only route mounts are gated), so + * the tables should exist in either mode. * * Boots the backend setups (registering the models), runs sync(), and asserts * the gated-feature tables are present — exits non-zero if any are missing. From d25ded8d92c1ef9a2e19b70d98abdab47566aeb4 Mon Sep 17 00:00:00 2001 From: Carson Davis Date: Mon, 29 Jun 2026 17:31:46 -0500 Subject: [PATCH 06/13] Reframe both-modes tables justification: pins the unconditional-sync invariant, not migration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The both-modes tables-exist check and the lean docs justified gated-off tables as 'so a mode flip needs no migration.' That reason doesn't hold: deployments never switch modes, and sequelize.sync() is additive and runs every boot. Reword to the accurate framing — only route mounts are gated; models register and sync unconditionally (ADR D2: keep, env-gated), so gated-off tables are created but unused. assert-gated-tables.js pins exactly that invariant (catches a model accidentally gated or dropped). Docs/comments only; no behavior change. --- .github/workflows/playwright-tests.yml | 10 ++++++---- AGENTS.md | 2 +- README.md | 2 +- tests/ci/assert-gated-tables.js | 13 +++++++++---- 4 files changed, 17 insertions(+), 10 deletions(-) diff --git a/.github/workflows/playwright-tests.yml b/.github/workflows/playwright-tests.yml index c83835ca0..a5556321d 100644 --- a/.github/workflows/playwright-tests.yml +++ b/.github/workflows/playwright-tests.yml @@ -113,10 +113,12 @@ jobs: - name: Initialize Database run: node scripts/init-db.js - # Features gated OFF in lean must still have their DB tables - # (model sync runs unconditionally; only route mounts are gated), so a - # later mode flip needs no data migration. This step boots the app once - # (which runs sequelize.sync()) and then asserts the gated tables exist. + # Features gated OFF in lean must still have their DB tables: model + # registration + sync run unconditionally, only route mounts are gated + # (ADR D2: keep, env-gated — models aren't per-mode-gated). This pins that + # invariant so a change that gates model registration, or drops a model, + # fails CI. This step boots the app once (which runs sequelize.sync()) and + # then asserts the gated tables exist. # No continue-on-error: a missing table fails the leg. - name: Assert gated tables exist run: node tests/ci/assert-gated-tables.js diff --git a/AGENTS.md b/AGENTS.md index 3e47232ab..0aa324949 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -14,7 +14,7 @@ Local development uses hot-reloading and therefore there is little reason to run MMGIS deploys in two shapes selected by `MMGIS_DEPLOYMENT_MODE` (resolved once at startup in `API/Backend/Utils/deploymentMode.js`): - **`full`** (default) — the complete application as shipped today. -- **`lean`** — a gated-down deployment: geodata management, the bundled sidecar services/proxy, the on-disk mission filesystem, the link shortener, and server-side raster utilities are turned OFF; the dashboard publish flow (Deployments) is turned ON. Models still sync in both modes, so the gated-off tables exist regardless of mode (no migration needed to flip modes). +- **`lean`** — a gated-down deployment: geodata management, the bundled sidecar services/proxy, the on-disk mission filesystem, the link shortener, and server-side raster utilities are turned OFF; the dashboard publish flow (Deployments) is turned ON. Models register and sync unconditionally in both modes (only the server routes are gated), so the gated-off tables are created but sit unused in lean rather than being absent. Contributor rule: **author your change for the full app first, then confirm lean still passes.** CI runs the e2e/boot suite once per mode (`.github/workflows/playwright-tests.yml`), with a present/absent check (`tests/e2e/deployment-mode.spec.js`) whose expected on/off mapping is hand-written from the feature inventory — keep it independent of any capability table. diff --git a/README.md b/README.md index 778a78632..fe0d07189 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ MMGIS builds from one codebase into two deployment shapes, selected by the `MMGIS_DEPLOYMENT_MODE` environment variable: - **`full`** (default) — the complete MMGIS application as shipped today. Used when the variable is unset. -- **`lean`** — a lighter, smaller-footprint deployment that deliberately turns a set of server features off (geodata management, the bundled sidecar services/proxy, the on-disk mission filesystem, the link shortener, server-side raster utilities) and turns on the dashboard publish flow. Database tables for the gated-off features are still created, so switching a deployment from one mode to the other needs no data migration. +- **`lean`** — a lighter, smaller-footprint deployment that deliberately turns a set of server features off (geodata management, the bundled sidecar services/proxy, the on-disk mission filesystem, the link shortener, server-side raster utilities) and turns on the dashboard publish flow. Database tables for the gated-off features are still created (only the server routes are gated off), so they sit unused in lean rather than being absent. CI runs the end-to-end / boot suite once per mode. Contributors should author changes for the full app first, then confirm lean still passes. diff --git a/tests/ci/assert-gated-tables.js b/tests/ci/assert-gated-tables.js index 78bae9a8e..fabbb94fc 100644 --- a/tests/ci/assert-gated-tables.js +++ b/tests/ci/assert-gated-tables.js @@ -1,10 +1,15 @@ /** * assert-gated-tables.js * - * A feature gated OFF in the current deployment mode must still have its DB - * tables, so flipping modes later needs no data migration. Model registration + - * sequelize.sync() run unconditionally on boot (only route mounts are gated), so - * the tables should exist in either mode. + * A feature gated OFF in the current deployment mode still has its DB tables, + * because model registration + sequelize.sync() run unconditionally on boot and + * only route mounts are gated (ADR D2: keep, env-gated — models aren't + * per-mode-gated, so a gated-off feature's tables are created but unused). + * + * This check PINS that invariant: a change that accidentally gates model + * registration, or drops a model thinking it's dead in lean, fails CI in the leg + * where the feature is gated off. (It is not about enabling a migration-free + * mode flip — deployments don't switch modes, and sync() self-heals on boot.) * * Boots the backend setups (registering the models), runs sync(), and asserts * the gated-feature tables are present — exits non-zero if any are missing. From cbf2d4250b807a6bd38cf5877230f480b5a1f42f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 30 Jun 2026 19:59:20 +0000 Subject: [PATCH 07/13] chore: bump version to 4.2.12-20260630 [version bump] --- configure/package.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/configure/package.json b/configure/package.json index b920c8496..3104b0ae1 100644 --- a/configure/package.json +++ b/configure/package.json @@ -1,6 +1,6 @@ { "name": "configure", - "version": "4.2.11-20260611", + "version": "4.2.12-20260630", "homepage": "./configure/build", "private": true, "dependencies": { diff --git a/package.json b/package.json index 89f96140f..28a19597e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mmgis", - "version": "4.2.11-20260611", + "version": "4.2.12-20260630", "description": "A web-based mapping and localization solution for science operation on planetary missions.", "homepage": "build", "repository": { From 108613a305f73d86c535556240df988befaed1af Mon Sep 17 00:00:00 2001 From: Carson Davis Date: Tue, 30 Jun 2026 21:52:38 -0500 Subject: [PATCH 08/13] simplify instructions to agents about modes --- AGENTS.md | 9 --------- README.md | 2 +- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 0aa324949..360ab55e4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -9,15 +9,6 @@ Use MCP tools when possible for code analysis, symbol navigation, and code modifications. Local development uses hot-reloading and therefore there is little reason to run `npm run build` for the user. -## Deployment modes (two shapes from one codebase) - -MMGIS deploys in two shapes selected by `MMGIS_DEPLOYMENT_MODE` (resolved once at startup in `API/Backend/Utils/deploymentMode.js`): - -- **`full`** (default) — the complete application as shipped today. -- **`lean`** — a gated-down deployment: geodata management, the bundled sidecar services/proxy, the on-disk mission filesystem, the link shortener, and server-side raster utilities are turned OFF; the dashboard publish flow (Deployments) is turned ON. Models register and sync unconditionally in both modes (only the server routes are gated), so the gated-off tables are created but sit unused in lean rather than being absent. - -Contributor rule: **author your change for the full app first, then confirm lean still passes.** CI runs the e2e/boot suite once per mode (`.github/workflows/playwright-tests.yml`), with a present/absent check (`tests/e2e/deployment-mode.spec.js`) whose expected on/off mapping is hand-written from the feature inventory — keep it independent of any capability table. - ## Project Overview **MMGIS** is a web-based mapping and localization solution for science operations on planetary missions, developed by NASA-AMMOS. It provides spatial data infrastructure for mission-critical geospatial visualization and collaboration, supporting both 2D (Leaflet) and 3D (Cesium) mapping with real-time multi-user collaboration. diff --git a/README.md b/README.md index fe0d07189..b11bca79f 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ MMGIS builds from one codebase into two deployment shapes, selected by the `MMGI - **`full`** (default) — the complete MMGIS application as shipped today. Used when the variable is unset. - **`lean`** — a lighter, smaller-footprint deployment that deliberately turns a set of server features off (geodata management, the bundled sidecar services/proxy, the on-disk mission filesystem, the link shortener, server-side raster utilities) and turns on the dashboard publish flow. Database tables for the gated-off features are still created (only the server routes are gated off), so they sit unused in lean rather than being absent. -CI runs the end-to-end / boot suite once per mode. Contributors should author changes for the full app first, then confirm lean still passes. +CI runs the end-to-end / boot suite once per mode. --- From 1d606f5d4fe6517e85ccfc62473556b689267122 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 1 Jul 2026 02:53:47 +0000 Subject: [PATCH 09/13] chore: bump version to 4.2.13-20260701 [version bump] --- configure/package.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/configure/package.json b/configure/package.json index 3104b0ae1..854d0d6d3 100644 --- a/configure/package.json +++ b/configure/package.json @@ -1,6 +1,6 @@ { "name": "configure", - "version": "4.2.12-20260630", + "version": "4.2.13-20260701", "homepage": "./configure/build", "private": true, "dependencies": { diff --git a/package.json b/package.json index 28a19597e..01b8efcaa 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mmgis", - "version": "4.2.12-20260630", + "version": "4.2.13-20260701", "description": "A web-based mapping and localization solution for science operation on planetary missions.", "homepage": "build", "repository": { From b01eebb343075605627972e96927ef643f195606 Mon Sep 17 00:00:00 2001 From: Carson Davis Date: Tue, 30 Jun 2026 22:17:37 -0500 Subject: [PATCH 10/13] improve comments --- .github/workflows/playwright-tests.yml | 64 +++++++++----------------- tests/e2e/deployment-mode.spec.js | 47 +++++++++---------- 2 files changed, 44 insertions(+), 67 deletions(-) diff --git a/.github/workflows/playwright-tests.yml b/.github/workflows/playwright-tests.yml index 570166303..d5e432b37 100644 --- a/.github/workflows/playwright-tests.yml +++ b/.github/workflows/playwright-tests.yml @@ -7,10 +7,9 @@ on: branches: [master, main, development] jobs: - # Unit tests run once, outside the deployment-mode matrix. They are hermetic - # (Vitest + jsdom, no DB/browser) and already cover BOTH modes within a single - # run by busting the require cache per-test (see tests/unit/deploymentMode.spec.js - # and bakeGuards.spec.js). Matrixing them would just waste CI time. + # Unit tests (Vitest + jsdom, no DB/browser). Run once, outside the matrix: + # they already cover both modes in a single run by reloading the mode module + # per test. See tests/unit/deploymentMode.spec.js and bakeGuards.spec.js. unit: timeout-minutes: 10 runs-on: ubuntu-latest @@ -30,10 +29,8 @@ jobs: - name: Run Unit Tests run: npx vitest run - # The e2e / app-boot leg runs once per deployment shape. The mode is resolved - # once at process start (deploymentMode.js), so each shape needs its own boot — - # hence the matrix. MMGIS_DEPLOYMENT_MODE is written into .env in the setup - # step below and read by both the server and the present/absent e2e test. + # E2e tests boot the real app. Mode is fixed at process start, so we boot once + # per mode via the matrix. MMGIS_DEPLOYMENT_MODE is written into .env below. test: name: e2e (${{ matrix.mode }}) timeout-minutes: 20 @@ -92,13 +89,9 @@ jobs: echo "HIDE_CONFIG=true" >> .env echo "MMGIS_DEPLOYMENT_MODE=${{ matrix.mode }}" >> .env - # The lean leg must boot WITHOUT the sidecar services it doesn't deploy. - # sample.env ships several WITH_* sidecars on (TIPG/TITILER/TITILER_PGSTAC/ - # VELOSERVER); leaving them on in lean would have init-db try to create the - # mmgis-stac catalog DB and run pypgstac — neither exists in the lean - # topology. deploymentMode/init-db already gate the STAC DB on isFull(), - # but we also turn the WITH_* flags off here so the lean leg's env honestly - # reflects a lean deployment (no sidecars, no spatial-catalog DB). + # Lean deploys no sidecars. sample.env ships them ON; left on, init-db would + # try to create the mmgis-stac catalog DB. Turn them off so the lean env + # matches a real lean deployment. - name: Disable sidecar services for the lean leg if: matrix.mode == 'lean' run: | @@ -108,48 +101,35 @@ jobs: echo "WITH_TITILER_PGSTAC=false" >> .env echo "WITH_VELOSERVER=false" >> .env - # In lean, this step also proves the spatial-catalog (mmgis-stac) DB is - # NOT created (it's gated on isFull()). + # In lean, also confirms the mmgis-stac catalog DB is not created (full-only). - name: Initialize Database run: node scripts/init-db.js - # Features gated OFF in lean must still have their DB tables: model - # registration + sync run unconditionally, only route mounts are gated - # (ADR D2: keep, env-gated — models aren't per-mode-gated). This pins that - # invariant so a change that gates model registration, or drops a model, - # fails CI. This step boots the app once (which runs sequelize.sync()) and - # then asserts the gated tables exist. - # No continue-on-error: a missing table fails the leg. + # A feature gated OFF in the current mode still keeps its DB tables (both + # directions: datasets/geodatasets/draw/shortener off in lean, deployments + # off in full). Models always register; only routes are gated (ADR D2). + # Fails CI if an expected table is missing. - name: Assert gated tables exist run: node tests/ci/assert-gated-tables.js - # The e2e suite drives the real front-end at `/`, which the server renders - # from build/index.pug. Without this build step the route 500s, every test - # times out waiting for window.mmgisAPI, and the job is cancelled at the - # 20-minute cap. Build must run before the playwright steps start the server. + # Must build before the server starts: the e2e tests hit `/`, which 500s + # without build/index.pug. - name: Build frontend - # GitHub Actions auto-sets CI=true, which makes react-scripts promote all - # build warnings to errors. MMGIS ships several intentional warnings (the - # optional dist/ theme assets and the unshipped AOI counties/cities geojson - # dirs loaded via require.context). The production Dockerfile builds with CI - # unset, so mirror that here — otherwise the build fails on non-fatal warnings. + # CI=false: GitHub sets CI=true, which makes react-scripts fail on MMGIS's + # intentional build warnings. The production Dockerfile builds with CI unset. run: npm run build env: CI: false - # Legacy smoke / event-bus e2e tests. These boot the full SPA and have - # historically been allowed to not block the build (continue-on-error), - # so a genuinely-flaky legacy assertion doesn't turn the leg red. Kept - # non-gating here to preserve that behavior; the gating happens in the - # mode-shape step below. + # Legacy smoke / event-bus tests, kept non-gating (continue-on-error) so a + # flaky old assertion doesn't redden the job. Real gate is the step below. - name: Run legacy E2E Tests (non-gating) run: npx playwright test tests/e2e/smoke.spec.js tests/e2e/eventbus-integration.spec.js --project=chromium continue-on-error: true - # The deployment-mode present/absent check MUST fail - # CI when a shape breaks. NO continue-on-error here — a mis-gated feature - # (wrongly on in lean, or wrongly off in full) turns this leg red. This is - # the step that makes "a breaking change to either shape fails CI" real. + # Gating check: each feature's route must be mounted (non-404) in its mode + # and absent (404) otherwise. Checks mount vs. 404 only, not behavior. No + # continue-on-error, so mis-gating fails the leg. - name: Run deployment-mode shape check (gating) run: npx playwright test tests/e2e/deployment-mode.spec.js --project=chromium diff --git a/tests/e2e/deployment-mode.spec.js b/tests/e2e/deployment-mode.spec.js index d78d172a7..69c539d62 100644 --- a/tests/e2e/deployment-mode.spec.js +++ b/tests/e2e/deployment-mode.spec.js @@ -9,10 +9,8 @@ import { test, expect, request } from '@playwright/test' * * HAND-WRITTEN feature inventory — IMPORTANT: the expected on/off mapping below * is written by a person from the deployment ADR, NOT read from any capability - * table or the gated code. That independence is the point: a test that takes its - * expected answers from the thing it tests can't catch a wrong entry. Route PATHS - * were looked up in the code, but which mode each belongs to is hand-asserted. - * Do NOT rewrite this to read its expectations from a capability table. + * table or the gated code. Do NOT rewrite this to read its expectations from a + * capability table. * * Present vs. absent discriminator (verified against the code): a MOUNTED route * answers from its handler — with AUTH=off that's a 200 guard-failure/data body, @@ -116,27 +114,26 @@ test.describe(`Deployment mode present/absent — MODE=${MODE}`, () => { for (const feature of FEATURES) { const belongsToRunningMode = feature.mode === MODE - test(`${feature.name} is ${ - belongsToRunningMode ? 'present' : 'absent' - } in ${MODE}`, async () => { - const status = await probe(api, feature) + test(`${feature.name} is ${belongsToRunningMode ? 'present' : 'absent' + } in ${MODE}`, async () => { + const status = await probe(api, feature) - if (belongsToRunningMode) { - // Reachable: the route is mounted, so it must NOT hit the - // catch-all 404. (It may legitimately answer 200 with a guard - // failure, real data, or a proxy/upstream error status.) - expect( - status, - `${feature.name} should be MOUNTED in ${MODE} (got ${status}); a 404 means the route is gone` - ).not.toBe(404) - } else { - // Gone: the route is unmounted and falls through to the - // app catch-all, which returns a real 404. - expect( - status, - `${feature.name} should be ABSENT in ${MODE} (got ${status}); anything but 404 means the route is still mounted` - ).toBe(404) - } - }) + if (belongsToRunningMode) { + // Reachable: the route is mounted, so it must NOT hit the + // catch-all 404. (It may legitimately answer 200 with a guard + // failure, real data, or a proxy/upstream error status.) + expect( + status, + `${feature.name} should be MOUNTED in ${MODE} (got ${status}); a 404 means the route is gone` + ).not.toBe(404) + } else { + // Gone: the route is unmounted and falls through to the + // app catch-all, which returns a real 404. + expect( + status, + `${feature.name} should be ABSENT in ${MODE} (got ${status}); anything but 404 means the route is still mounted` + ).toBe(404) + } + }) } }) From 8d6021802b2f35e85d9e8c2fa392e182929334e1 Mon Sep 17 00:00:00 2001 From: Carson Davis Date: Tue, 30 Jun 2026 22:39:46 -0500 Subject: [PATCH 11/13] Tighten docstrings in deployment-mode spec and gated-tables check Trim verbose comments to terse notes; keep the non-obvious rationale (ADR-independent mapping, mounted-vs-404 discriminator, init-db ordering) and drop restated prose. Comments only; no behavior change. --- tests/ci/assert-gated-tables.js | 29 +++++++------------ tests/e2e/deployment-mode.spec.js | 47 ++++++++++--------------------- 2 files changed, 25 insertions(+), 51 deletions(-) diff --git a/tests/ci/assert-gated-tables.js b/tests/ci/assert-gated-tables.js index fabbb94fc..73c31572f 100644 --- a/tests/ci/assert-gated-tables.js +++ b/tests/ci/assert-gated-tables.js @@ -1,22 +1,16 @@ /** * assert-gated-tables.js * - * A feature gated OFF in the current deployment mode still has its DB tables, - * because model registration + sequelize.sync() run unconditionally on boot and - * only route mounts are gated (ADR D2: keep, env-gated — models aren't - * per-mode-gated, so a gated-off feature's tables are created but unused). + * A feature gated OFF in the current mode still keeps its DB tables: models + * register and sync() runs unconditionally on boot, only route mounts are gated + * (ADR D2). This pins that — dropping a model, or gating its registration, fails + * CI in the leg where the feature is off. * - * This check PINS that invariant: a change that accidentally gates model - * registration, or drops a model thinking it's dead in lean, fails CI in the leg - * where the feature is gated off. (It is not about enabling a migration-free - * mode flip — deployments don't switch modes, and sync() self-heals on boot.) - * - * Boots the backend setups (registering the models), runs sync(), and asserts - * the gated-feature tables are present — exits non-zero if any are missing. + * Boots the backend setups (registering models), runs sync(), and exits non-zero + * if any gated-feature table is missing. * * Must run AFTER scripts/init-db.js: sync() creates PostGIS geometry columns, so - * the postgis extension has to exist first or it throws - * `type "geometry" does not exist`. + * the postgis extension must exist first. */ require("dotenv").config({ path: __dirname + "/../../.env" }); @@ -25,10 +19,8 @@ const { MODE } = require("../../API/Backend/Utils/deploymentMode"); const setups = require("../../API/setups"); const { sequelize } = require("../../API/connection"); -// Hand-written from the deployment feature inventory: the tables behind the -// features that are gated OFF in one mode or the other. They must exist in BOTH -// modes. The shortener model is `url_shortener`; Sequelize pluralizes it to -// `url_shorteners` by default, so accept either spelling. +// Hand-written: tables behind features gated off in one mode or the other; they +// must exist in BOTH. Accept either spelling where Sequelize may pluralize. const REQUIRED_TABLE_GROUPS = [ ["datasets"], // geodata management (datasets) ["geodatasets"], // geodata management (geodatasets) @@ -40,8 +32,7 @@ const REQUIRED_TABLE_GROUPS = [ async function main() { await new Promise((resolve) => { - // Loading the backend setups requires each feature's setup.js, which in - // turn requires its models — registering them on the shared sequelize. + // Loading the setups requires each feature's models, registering them. setups.getBackendSetups(() => resolve()); }); diff --git a/tests/e2e/deployment-mode.spec.js b/tests/e2e/deployment-mode.spec.js index 69c539d62..cd1ad9af5 100644 --- a/tests/e2e/deployment-mode.spec.js +++ b/tests/e2e/deployment-mode.spec.js @@ -1,37 +1,28 @@ import { test, expect, request } from '@playwright/test' /** - * Deployment-mode present/absent checks. + * Deployment-mode present/absent checks. Boots once per mode in CI; per feature, + * asserts its route is reachable in the mode it belongs to and gone otherwise. * - * The CI matrix boots the app once per shape (MMGIS_DEPLOYMENT_MODE=full|lean); - * this test reads the mode and, per feature, asserts the feature is reachable - * when it belongs to the running mode and gone otherwise. + * The on/off mapping below is hand-written from the deployment ADR. Do NOT + * rewrite it to read expectations from a capability table — a test that takes its + * answers from the code under test can't catch a wrong entry. * - * HAND-WRITTEN feature inventory — IMPORTANT: the expected on/off mapping below - * is written by a person from the deployment ADR, NOT read from any capability - * table or the gated code. Do NOT rewrite this to read its expectations from a - * capability table. - * - * Present vs. absent discriminator (verified against the code): a MOUNTED route - * answers from its handler — with AUTH=off that's a 200 guard-failure/data body, - * or a proxy/upstream error for sidecars; never 404. An UNMOUNTED route falls - * through to the catch-all `app.all('*')` -> 404. So: mounted => status !== 404; - * absent => 404. + * Discriminator: a mounted route answers from its handler (never 404); an + * unmounted route hits the catch-all `app.all('*')` -> 404. */ const MODE = process.env.MMGIS_DEPLOYMENT_MODE || 'full' -// Each feature: where it belongs, and a single HTTP probe that exercises a route -// the feature owns. `method`/`path` only — no body needed, because we only care -// whether the route is mounted (any non-404) or absent (404). +// Each feature: which mode it belongs to, and one HTTP probe against a route it +// owns. We only care whether that route is mounted (non-404) or absent (404). const FEATURES = [ // --- Full-only: present in full, absent in lean --- { name: 'geodata management (datasets)', mode: 'full', method: 'post', - // ensureAdmin allow-lists /api/datasets/get, so in full it reaches the - // mounted router even without admin auth. + // ensureAdmin allow-lists /get, so it reaches the router without auth. path: '/api/datasets/get', }, { @@ -63,18 +54,14 @@ const FEATURES = [ name: 'server-side raster utilities', mode: 'full', method: 'post', - // /api/utils is mounted in both modes, but the raster endpoints inside - // it (getbands/getprofile/...) are registered only when isFull(). + // /api/utils mounts in both modes, but its raster endpoints are isFull()-only. path: '/api/utils/getbands', }, { name: 'bundled sidecar services / proxy (titiler)', mode: 'full', method: 'get', - // The /titiler proxy is mounted only when isFull() AND WITH_TITILER=true - // (the full CI leg keeps the sample.env sidecar flags on). The lean leg - // turns the WITH_* flags off and the proxy is also isFull()-gated, so it - // is absent there. + // /titiler proxy needs isFull() AND WITH_TITILER=true; lean has neither. path: '/titiler/healthz', }, @@ -83,8 +70,7 @@ const FEATURES = [ name: 'dashboard publish flow (deployments)', mode: 'lean', method: 'get', - // Mounted only when isLean(); ensureAdmin rejects with a 200 JSON body - // (not 404) when present. + // Mounted only when isLean(); ensureAdmin rejects with 200 (not 404). path: '/api/deployments', }, ] @@ -119,16 +105,13 @@ test.describe(`Deployment mode present/absent — MODE=${MODE}`, () => { const status = await probe(api, feature) if (belongsToRunningMode) { - // Reachable: the route is mounted, so it must NOT hit the - // catch-all 404. (It may legitimately answer 200 with a guard - // failure, real data, or a proxy/upstream error status.) + // Mounted: any status but the catch-all 404. expect( status, `${feature.name} should be MOUNTED in ${MODE} (got ${status}); a 404 means the route is gone` ).not.toBe(404) } else { - // Gone: the route is unmounted and falls through to the - // app catch-all, which returns a real 404. + // Absent: falls through to the app catch-all 404. expect( status, `${feature.name} should be ABSENT in ${MODE} (got ${status}); anything but 404 means the route is still mounted` From 51acd0565c5b5480d909c4538d0c77c3fa97e5f3 Mon Sep 17 00:00:00 2001 From: Carson Davis Date: Tue, 30 Jun 2026 22:45:39 -0500 Subject: [PATCH 12/13] Load .env in Playwright config so the mode reaches the test process MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit deployment-mode.spec.js reads MMGIS_DEPLOYMENT_MODE from process.env, but the Playwright runner never loaded .env — so the spec defaulted to 'full' and the lean CI leg tested the wrong mode (all assertions failed). Mirror scripts/server.js by loading dotenv in the config. Verified via --list: the spec now resolves MODE=lean from .env. --- playwright.config.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/playwright.config.js b/playwright.config.js index 0024e1fad..f780ea34a 100644 --- a/playwright.config.js +++ b/playwright.config.js @@ -1,3 +1,8 @@ +// Load .env so the test process sees the same vars as the server — notably +// MMGIS_DEPLOYMENT_MODE, which deployment-mode.spec.js reads from process.env. +// Without this the spec defaults to 'full' and the lean CI leg tests the wrong +// mode. Mirrors scripts/server.js, which also loads .env via dotenv. +import "dotenv/config"; import { defineConfig, devices } from "@playwright/test"; /** From 9aefadcfaaaf5f7fc391994216f08f5559309b6f Mon Sep 17 00:00:00 2001 From: Carson Davis Date: Wed, 1 Jul 2026 11:54:44 -0500 Subject: [PATCH 13/13] ci(e2e): keep legacy smoke/eventbus specs gating They ran gated on development (no continue-on-error). The matrix split accidentally added continue-on-error: true, silently downgrading their coverage. Restore gating so a real regression fails the leg; they also now confirm smoke + event bus pass under lean, not just full. --- .github/workflows/playwright-tests.yml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/playwright-tests.yml b/.github/workflows/playwright-tests.yml index d5e432b37..05eba5a64 100644 --- a/.github/workflows/playwright-tests.yml +++ b/.github/workflows/playwright-tests.yml @@ -121,11 +121,12 @@ jobs: env: CI: false - # Legacy smoke / event-bus tests, kept non-gating (continue-on-error) so a - # flaky old assertion doesn't redden the job. Real gate is the step below. - - name: Run legacy E2E Tests (non-gating) + # Legacy smoke / event-bus specs. Gating (no continue-on-error), matching + # their pre-matrix behavior on development — a real regression here fails + # the leg. They boot the same app per mode, so they also confirm smoke + + # event bus pass under lean, not just full. + - name: Run legacy E2E Tests run: npx playwright test tests/e2e/smoke.spec.js tests/e2e/eventbus-integration.spec.js --project=chromium - continue-on-error: true # Gating check: each feature's route must be mounted (non-404) in its mode # and absent (404) otherwise. Checks mount vs. 404 only, not behavior. No