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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 77 additions & 10 deletions .github/workflows/playwright-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we upgrade this version?

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:
Expand Down Expand Up @@ -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

Expand All @@ -62,26 +90,65 @@ 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

- name: Upload test artifacts
if: always()
uses: actions/upload-artifact@v4
with:
name: test-results
name: test-results-${{ matrix.mode }}
path: test-results/
retention-days: 7
9 changes: 9 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

---
Expand Down
95 changes: 95 additions & 0 deletions tests/ci/assert-gated-tables.js
Original file line number Diff line number Diff line change
@@ -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);
});
Loading