aab58a1c24
Nine test files on the server, none on the client, and no test script in the root package at all — so CI's only gate on the half of this project that runs in a stranger's browser was `tsc --noEmit`, which will tell you the types line up and nothing about whether an anonymous visitor is handed the private office. Both files here were built to be tested and never were. `access.ts` is the only module in the bundle whose output is a set of decisions about what a stranger may see, and its own header carries the reason a test is owed: a shipped line read `canEnterOffice = s.authenticated || !s.passwordLogin`, which is true for `auth: none` and dangerously false for `sso` — where `POST /session` answers 404 precisely *because* credentials are issued elsewhere — so on an SSO deployment every anonymous visitor got the private view while the config still said the box was private. There is now a test named after that bug. The assertions are weighted toward the closed direction on purpose: one that a member gets what a member is owed, and half a dozen that nobody gets more than nothing, because showing a member the public office is a bad afternoon and showing a stranger the private one is what the tiers exist to prevent. The 5xx case is in there too — a box mid-restart is `anon`, not `member` — and so is the `javascript:` entry URL, which a CSP of `script-src 'self' 'unsafe-inline'` does not stop from navigating. `plan.ts` says in its own header that it imports no three.js "so the splitting pass is testable without a WebGL context", and that it "drops rather than throws … every one of those is reported through `problems`" — an array whose whole purpose is to be asserted on, which nothing asserted on. So: a pack wrong in five ways still builds and files five reports; a zero-length wall does not put a NaN in the bounds; an office with no levels stays finite so nothing downstream divides by it; a pack with the required arrays missing is taken, because HTTP will send one. And the wall pass gets the check CONTRACT.md §2's argument deserves — one decomposition, two products — by walking a walker through the door and into the wall beside it, and through a window and being stopped. Also the yaw convention, which nothing stated and `officeMinimap.ts` draws straight from: get the sign wrong and every wall mirrors about its own centre, invisible on a square and obvious on anything else. No new dependency. The server already runs `node --test` over `.ts` on native type stripping, so the client does the same — which matters here, because this repo's "no surprise dependencies" check is an allowlist naming why each one is permitted, and a test runner would have needed an entry and an argument. One source change was needed to make any of it possible. `session.ts` read `import.meta.env.VITE_IDENTITY_URL` at module scope, and `access.ts` imports `authFetch` from it — so one property access made the file that decides what an anonymous visitor sees unreachable from a plain test runner, which is most of why it had no tests. It now reads the way `plan.ts` already reads `DEV`, by the idiom that file documents as being there "so this module stays importable from a plain test runner". 31 tests, 9 suites, all passing, wired into `npm test` and into the CI job beside the server's. `vite build` is unchanged and no test code reaches the bundle.
123 lines
4.6 KiB
YAML
123 lines
4.6 KiB
YAML
# The gates.
|
|
#
|
|
# This repo makes two claims about itself — that it is Apache-2.0 clean, and that
|
|
# a stranger can self-host it with no account, no key and no network — and until
|
|
# this file existed nothing checked either one. Both had been verified per-lens,
|
|
# in a unit test or by hand on a box that already had everything, and never once
|
|
# end to end on an empty environment. A design critic put it exactly right:
|
|
# until one of these jobs exists, self-hostable is a design intention, not a
|
|
# property.
|
|
#
|
|
# Three jobs, one for each claim that can actually be measured:
|
|
#
|
|
# clean-clone a fresh checkout installs, builds and passes its tests
|
|
# zero-config-boot the API answers health when handed nothing at all
|
|
# no-binary-art src/** carries no committed binary assets
|
|
#
|
|
# They are deliberately independent and run in parallel: a broken build should
|
|
# not hide a licensing regression.
|
|
#
|
|
# This runs on Gitea Actions, not GitHub. The syntax is GitHub-compatible and the
|
|
# `actions/*` steps resolve through the instance's configured action registry.
|
|
|
|
name: gates
|
|
|
|
on:
|
|
push:
|
|
branches: [master]
|
|
pull_request:
|
|
workflow_dispatch:
|
|
|
|
# A superseded push has nothing to tell us, and these jobs build a Docker image
|
|
# and bind a port between them.
|
|
concurrency:
|
|
group: gates-${{ github.ref }}
|
|
cancel-in-progress: true
|
|
|
|
jobs:
|
|
# ---- Job 1: a stranger clones this and it works ----
|
|
#
|
|
# The whole job is the dev-kit promise from CONTRACT.md §0. Note what is absent
|
|
# and is meant to stay absent: no `env:` block, no `secrets.*` anywhere, and no
|
|
# dependency cache. The cache is the interesting omission — a warm cache would
|
|
# make this job pass on a lockfile that no longer resolves from a clean state,
|
|
# which is precisely the failure a stranger would hit first.
|
|
clean-clone:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
|
|
- uses: actions/setup-node@v4
|
|
with:
|
|
# The server runs TypeScript directly by stripping types at load, so
|
|
# the floor is real: server/package.json says >=22.18. 24 is what the
|
|
# image in server/Dockerfile uses.
|
|
node-version: "24"
|
|
|
|
- name: npm ci
|
|
run: npm ci
|
|
|
|
- name: npm run build
|
|
# tsc --noEmit, then vite build. A dynamic import of an uninstalled
|
|
# package fails here, which is the reason the dependency policy is an
|
|
# allowlist rather than a count — see CONTRACT.md §6.
|
|
run: npm run build
|
|
|
|
- name: client tests
|
|
run: npm test
|
|
|
|
- name: server tests
|
|
run: npm test --workspace @lumbridge/tera-api
|
|
|
|
# ---- Job 2: the box boots on nothing ----
|
|
#
|
|
# Two independent server designs defaulted the weather source to a provider
|
|
# that needs a contact string and then failed hard without one, which breaks
|
|
# the only acceptance test this repo has. This job exists specifically to catch
|
|
# that class of bug, and it passes today.
|
|
#
|
|
# The gate starts the real entry point under a genuinely empty environment
|
|
# rather than `docker compose up`, which is the wording in CONTRACT.md §0. The
|
|
# property asserted is identical — the compose file adds only TERA_HOST and
|
|
# container hardening, and every other variable in it carries a `:-` default so
|
|
# an empty environment resolves it to the empty string — while a job that needs
|
|
# docker-in-docker present on the runner would be measuring the runner instead
|
|
# of the repo. `node scripts/check-zero-config-boot.mjs --compose` runs the
|
|
# literal compose form for anyone who has a Docker.
|
|
zero-config-boot:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
|
|
- uses: actions/setup-node@v4
|
|
with:
|
|
node-version: "24"
|
|
|
|
- name: npm ci
|
|
run: npm ci
|
|
|
|
- name: health on an empty environment
|
|
run: node scripts/check-zero-config-boot.mjs
|
|
|
|
# ---- Job 3: no committed binary art ----
|
|
#
|
|
# Scoped strictly to tracked files under src/**, because a self-hoster is told
|
|
# to put their own legally-clean art in public/props/ and friends and an
|
|
# earlier design would have failed their build for doing exactly that. The
|
|
# script enumerates with `git ls-files` and has no filesystem-walk fallback.
|
|
# See CONTRACT.md §7.
|
|
#
|
|
# No install step: the check is dependency-free on purpose, so it stays
|
|
# runnable by hand and cannot be broken by a bad lockfile.
|
|
no-binary-art:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
|
|
- uses: actions/setup-node@v4
|
|
with:
|
|
node-version: "24"
|
|
|
|
- name: no binary art under src/
|
|
run: node scripts/check-no-binaries.mjs
|