name: Tests permissions: contents: read on: pull_request: branches: [main, dev] paths: - 'server/**' - 'client/**' - 'shared/**' - 'plugin-sdk/**' # npm workspaces hoist every dependency into the single root lockfile, so # a dependency bump touches none of the four directories above — and a # native binding like better-sqlite3 is exactly what needs the suite. - 'package.json' - 'package-lock.json' - 'Dockerfile' - 'docker-compose.yml' - '.github/workflows/test.yml' jobs: plugin-facts: name: Plugin Permission Facts runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 22 cache: npm - name: Install dependencies run: npm ci --workspace server # The plugin permission facts live once, in # server/src/nest/plugins/protocol/envelope.ts. Every other copy - the SDK's # generated/host-facts.ts and shared's plugin-permissions.ts - is generated from it # and checked in, because publish-plugin-sdk builds from a standalone checkout with # no server present. This fails the PR if a copy drifts, if a new permission has no # admin.plugins.perm.* translation, or if the CLI has no hint for it. - name: Check the generated plugin facts are in sync run: cd server && npm run check:plugin-facts i18n-parity: name: i18n Key Parity runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - uses: actions/setup-node@v6 with: node-version: 24 - name: Check i18n key parity run: node shared/scripts/i18n-parity.mjs --strict shared-contracts: name: Shared Contracts (Zod) runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - uses: actions/setup-node@v6 with: node-version: 24 cache: npm cache-dependency-path: package-lock.json - name: Install dependencies run: npm ci --workspace shared - name: Typecheck run: cd shared && npm run typecheck - name: Run tests run: cd shared && npm run test:coverage - name: Upload coverage if: success() uses: actions/upload-artifact@v6 with: name: shared-coverage path: shared/coverage/ retention-days: 7 plugin-sdk-tests: name: Plugin SDK Tests runs-on: ubuntu-latest defaults: run: working-directory: plugin-sdk steps: - uses: actions/checkout@v6 - uses: actions/setup-node@v6 with: # 22 matches the TREK server runtime and has node:sqlite, which the # dev-server tests exercise (same choice as publish-plugin-sdk.yml). node-version: 22 # plugin-sdk is standalone (own lockfile, not a root workspace), so it # installs from its own directory. - name: Install dependencies run: npm ci - name: Typecheck run: npm run typecheck - name: Run tests run: npm run test:coverage - name: Upload coverage if: success() uses: actions/upload-artifact@v6 with: name: plugin-sdk-coverage path: plugin-sdk/coverage/ retention-days: 7 server-tests: name: Server Tests runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - uses: actions/setup-node@v6 with: node-version: 24 cache: npm cache-dependency-path: package-lock.json - name: Install dependencies run: npm ci - name: Ensure @swc/core's Linux binary for unplugin-swc # The lockfile was generated on Windows and omits @swc/core's Linux # optional native binary, so npm ci/install skips it on the runner. # Install the matching version explicitly so the server's SWC transform # (server/vitest.config.ts) can load. run: | SWC_VERSION=$(node -p "require('@swc/core/package.json').version") npm install --no-save --legacy-peer-deps "@swc/core-linux-x64-gnu@$SWC_VERSION" - name: Build shared run: npm run build --workspace=shared - name: Build server (tsc -> dist) run: cd server && npm run build - name: Smoke production require chain # Vitest aliases the MCP SDK deep imports, so only this exercises what # production runs: the built dist resolving the MCP SDK's subpath # exports through the tsconfig-paths/register runtime hook. run: cd server && node --require tsconfig-paths/register -e "require('./dist/nest-mcp')" - name: Typecheck run: cd server && npm run typecheck - name: Typecheck tests # tests/ is outside the build tsconfig, so a service that grows a # constructor parameter does not break its hand-wired call sites at # compile time: the argument arrives as undefined and throws at runtime, # if anything reads it at all. This is the step that catches it. run: cd server && npm run typecheck:tests - name: Lint run: cd server && npm run lint:check - name: Run tests run: cd server && npm run test:coverage - name: Upload coverage if: success() uses: actions/upload-artifact@v6 with: name: backend-coverage path: server/coverage/ retention-days: 7 s3-contract: name: S3 Driver Contract (MinIO) runs-on: ubuntu-latest # Fork PRs don't get repo secrets, so MINIO_LICENSE_KEY would be empty # and the AIStor container would fail to boot. if: github.event.pull_request.head.repo.full_name == github.repository steps: - uses: actions/checkout@v6 - uses: actions/setup-node@v6 with: node-version: 24 cache: npm cache-dependency-path: package-lock.json - name: Install dependencies run: npm ci - name: Ensure @swc/core's Linux binary for unplugin-swc # The lockfile was generated on Windows and omits @swc/core's Linux # optional native binary, so npm ci/install skips it on the runner. # Install the matching version explicitly so the server's SWC transform # (server/vitest.config.ts) can load. run: | SWC_VERSION=$(node -p "require('@swc/core/package.json').version") npm install --no-save --legacy-peer-deps "@swc/core-linux-x64-gnu@$SWC_VERSION" - name: Build shared run: npm run build --workspace=shared - name: Write MinIO license run: printf '%s' "${{ secrets.MINIO_LICENSE_KEY }}" > minio-licence.key - name: Start MinIO # `services:` can't pass the `server /data --license ...` command to # the AIStor image, so it starts via docker run. 127.0.0.1 endpoint is # load-bearing: aws-lite picks path-style only for IP/localhost hosts, # which is all stock MinIO/AIStor speaks. run: | docker run -d --name minio -p 127.0.0.1:9000:9000 \ -v "$PWD/minio-licence.key:/minio.license:ro" \ -e MINIO_ROOT_USER=trekci -e MINIO_ROOT_PASSWORD=trekci-secret \ quay.io/minio/aistor/minio:RELEASE.2026-08-07T18-34-35Z server /data --license /minio.license - name: Wait for MinIO # curl was removed from the AIStor image — poll from the runner instead. # 60 x 2s gives a wider margin on cold runners than the 30 x 1s the # pre-6fbff835 job used. run: | for i in $(seq 1 60); do curl -sf http://127.0.0.1:9000/minio/health/live && exit 0 sleep 2 done echo "MinIO never became healthy" >&2 docker logs minio >&2 exit 1 - name: Run the S3 driver contract suite # No --coverage: the src/nest/storage/** 94/84/97/94 pin must keep # holding from hermetic tests alone. The suite's beforeAll creates # the trek-test bucket idempotently — no separate bucket-init step. env: TREK_TEST_S3_ENDPOINT: http://127.0.0.1:9000 TREK_TEST_S3_BUCKET: trek-test TREK_TEST_S3_ACCESS_KEY_ID: trekci TREK_TEST_S3_SECRET_ACCESS_KEY: trekci-secret run: cd server && npx vitest run tests/unit/nest/storage/s3.driver.contract.test.ts client-quality: # Split out of client-tests: the suite takes ~11 minutes, and having the # gates in front of it meant a single lint finding threw away the whole test # signal for that run. Both jobs pay the install/build, which is cheap next # to running the two in series. name: Client Types & Lint runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - uses: actions/setup-node@v6 with: node-version: 24 cache: npm cache-dependency-path: package-lock.json - name: Install dependencies run: npm ci --workspace shared && npm ci --workspace client - name: Build shared run: npm run build --workspace=shared - name: Typecheck run: cd client && npm run typecheck - name: Lint run: cd client && npm run lint:check - name: Page pattern check run: cd client && npm run lint:pages client-tests: name: Client Tests runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - uses: actions/setup-node@v6 with: node-version: 24 cache: npm cache-dependency-path: package-lock.json - name: Install dependencies run: npm ci --workspace shared && npm ci --workspace client - name: Build shared run: npm run build --workspace=shared - name: Run tests run: cd client && npm run test:coverage - name: Upload coverage if: success() uses: actions/upload-artifact@v6 with: name: frontend-coverage path: client/coverage/ retention-days: 7