Phase 2 of docs/modules/rust/PLAN.md. Phase 1 built five guards and ran them by hand; this repo had no workflows at all, so nothing gated the branch that gets released and there was no way to release it. Three pieces: - **release.yml** — the derived-version engine link, installer and Module-uo already run (conventional-commit subjects since the newest tag; module.json's version survives as a floor; workflow_dispatch as the backdoor), assembling the bundle from an include list and publishing the tarball, the install manifest carrying its sha256, and SHA256SUMS. The tag is the number that ships and CI stamps it into the bundle's own module.json. - **pr-checks.yml** — server tests, check:imports, check:bundle, check:swagger, the client build, client tests and check:externals, plus frozen-manifest. - **frozen-manifest** — clones core at the sha pinned in ci/core-ref.json, generates its route table without this module and with it, and takes the difference. It ran locally against that exact ref: six routes, all documented, no core route moved. That is the first proof by a running core that /rust collides with nothing — phase 1 could only check it by reading, because core mounts /status and /version at a tier root where the loader's own collision probe cannot see them. The bundle carries no node_modules, because the shipped half declares no runtime dependencies (org lead, phase 2). checkBundle.js holds both halves of that: the include list still covers everything server/index.js reaches, and no dependency has appeared without the release learning to pack it. Verified by breaking it — dropping "model" from the list names the exact edit and exits 1. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
178 lines
8.7 KiB
JavaScript
178 lines
8.7 KiB
JavaScript
// The frozen manifest's derivation, checked.
|
|
//
|
|
// `scripts/frozenManifest.js` runs in one place — a CI job with a whole core
|
|
// checked out beside it — so it is the least-exercised piece of machinery in this
|
|
// repo, and it is the piece that decides whether the URLs this module claims are
|
|
// the URLs it serves (MODULE_API.md §5.3). Its three answers are pure functions of
|
|
// two manifests and a fragment, so all three are asked here, with fixtures rather
|
|
// than a clone.
|
|
//
|
|
// What is deliberately NOT asserted here: the numbers. `routes.manifest.json`'s
|
|
// routes are proved by the job that generates them from a real core, and a copy of
|
|
// that count in this file would only ever be a second thing to update.
|
|
|
|
const test = require('node:test')
|
|
const assert = require('node:assert')
|
|
const fs = require('node:fs')
|
|
const path = require('node:path')
|
|
|
|
const { diffManifests, coverage, MANIFEST, FRAGMENT } = require('../scripts/frozenManifest')
|
|
|
|
const manifest = (public_ = [], internal = []) => ({ public: public_, internal })
|
|
const get = (p) => ({ method: 'GET', path: p })
|
|
|
|
test("the module's routes are the ones a core gains by loading it", () => {
|
|
const before = manifest([get('/api/v1/public/settings')])
|
|
const after = manifest([get('/api/v1/public/settings'), get('/api/v1/public/rust/servers')])
|
|
|
|
const { added, removed } = diffManifests(before, after)
|
|
assert.deepStrictEqual(removed, [])
|
|
assert.deepStrictEqual(added, [{ method: 'GET', path: '/api/v1/public/rust/servers', tier: 'public' }])
|
|
})
|
|
|
|
test('a route core loses to the module is reported, not quietly absorbed', () => {
|
|
// The failure this exists for, and the one phase 1 could only check by reading:
|
|
// core mounts several routes at the TIER ROOT (/status, /version) that the
|
|
// loader's collision probe cannot see, so a module whose mount displaced one
|
|
// would not show up as an addition — the URL is unchanged — and a check that
|
|
// only looked at what appeared would call it clean.
|
|
const before = manifest([get('/api/v1/public/settings'), get('/api/v1/public/status')])
|
|
const after = manifest([get('/api/v1/public/settings')])
|
|
|
|
const { removed } = diffManifests(before, after)
|
|
assert.deepStrictEqual(removed, ['public GET /api/v1/public/status'])
|
|
})
|
|
|
|
test('a route whose METHOD changed counts as removed and added', () => {
|
|
const { added, removed } = diffManifests(
|
|
manifest([{ method: 'POST', path: '/api/v1/admin/thing' }]),
|
|
manifest([{ method: 'PUT', path: '/api/v1/admin/thing' }]),
|
|
)
|
|
assert.deepStrictEqual(removed, ['public POST /api/v1/admin/thing'])
|
|
assert.strictEqual(added.length, 1)
|
|
})
|
|
|
|
test('the internal app is diffed too, and keeps its own tier', () => {
|
|
const { added } = diffManifests(
|
|
manifest([], [get('/internal/health')]),
|
|
manifest([], [get('/internal/health'), get('/internal/rust/thing')]),
|
|
)
|
|
assert.deepStrictEqual(added, [{ method: 'GET', path: '/internal/rust/thing', tier: 'internal' }])
|
|
})
|
|
|
|
test('added routes are sorted, so the committed file does not churn on traversal order', () => {
|
|
const { added } = diffManifests(
|
|
manifest([]),
|
|
manifest([get('/b'), get('/a'), { method: 'POST', path: '/a' }]),
|
|
)
|
|
assert.deepStrictEqual(
|
|
added.map((r) => `${r.method} ${r.path}`),
|
|
['GET /a', 'GET /b', 'POST /a'],
|
|
)
|
|
})
|
|
|
|
// ── coverage: the route ⇄ fragment agreement ────────────────────────────────
|
|
|
|
const fragment = (paths) => ({ paths })
|
|
|
|
test('a served route with no documented operation is named', () => {
|
|
const { undocumented, unserved } = coverage([get('/api/v1/public/rust/servers')], fragment({}))
|
|
assert.deepStrictEqual(undocumented, ['GET /api/v1/public/rust/servers'])
|
|
assert.deepStrictEqual(unserved, [])
|
|
})
|
|
|
|
test('a documented operation nobody serves is named too', () => {
|
|
// The direction core's own spec has no check for, which is how it accumulated
|
|
// orphan tags and schemas describing routes that had moved out of it. A
|
|
// documented URL nobody serves is a client following the docs into a 404.
|
|
const { undocumented, unserved } = coverage([], fragment({ '/api/v1/public/rust/gone': { get: {} } }))
|
|
assert.deepStrictEqual(undocumented, [])
|
|
assert.deepStrictEqual(unserved, ['GET /api/v1/public/rust/gone'])
|
|
})
|
|
|
|
test('express :params and OpenAPI {params} are the same route', () => {
|
|
const { undocumented, unserved } = coverage(
|
|
[{ method: 'DELETE', path: '/api/v1/admin/rust/servers/:id' }],
|
|
fragment({ '/api/v1/admin/rust/servers/{id}': { delete: {} } }),
|
|
)
|
|
assert.deepStrictEqual(undocumented, [])
|
|
assert.deepStrictEqual(unserved, [])
|
|
})
|
|
|
|
test('methods are matched, not just paths', () => {
|
|
const { undocumented, unserved } = coverage(
|
|
[{ method: 'POST', path: '/api/v1/admin/rust/servers/:id/test' }],
|
|
fragment({ '/api/v1/admin/rust/servers/{id}/test': { get: {} } }),
|
|
)
|
|
assert.deepStrictEqual(undocumented, ['POST /api/v1/admin/rust/servers/{id}/test'])
|
|
assert.deepStrictEqual(unserved, ['GET /api/v1/admin/rust/servers/{id}/test'])
|
|
})
|
|
|
|
// ── the committed artifacts, against each other ─────────────────────────────
|
|
//
|
|
// These two files are generated together by a job that has a real core; here
|
|
// there is no core, so what can still be asked is whether they agree with each
|
|
// other. If they do not, one of them was committed without the other.
|
|
|
|
test('every route in the committed manifest has a committed operation', () => {
|
|
const { routes } = JSON.parse(fs.readFileSync(MANIFEST, 'utf8'))
|
|
const spec = JSON.parse(fs.readFileSync(FRAGMENT, 'utf8'))
|
|
const { undocumented, unserved } = coverage(routes, spec)
|
|
assert.deepStrictEqual(undocumented, [], 'routes.manifest.json lists routes swagger-fragment.json does not document')
|
|
assert.deepStrictEqual(unserved, [], 'swagger-fragment.json documents operations routes.manifest.json does not list')
|
|
})
|
|
|
|
test('the fragment carries only the three sections §6.1a allows', () => {
|
|
const spec = JSON.parse(fs.readFileSync(FRAGMENT, 'utf8'))
|
|
assert.deepStrictEqual(Object.keys(spec).sort(), ['components', 'paths', 'tags'])
|
|
assert.deepStrictEqual(Object.keys(spec.components), ['schemas'])
|
|
})
|
|
|
|
test("the fragment defines only namespaced schemas, and redefines none of core's", () => {
|
|
const spec = JSON.parse(fs.readFileSync(FRAGMENT, 'utf8'))
|
|
for (const name of Object.keys(spec.components.schemas)) {
|
|
assert.match(name, /^Rust[A-Z]/, `${name} is not namespaced — core wins the collision and drops it (§6.1a)`)
|
|
}
|
|
// Anything this fragment REFERENCES and does not define has to be one of
|
|
// core's shared schemas, which resolve in the merged document — that is the
|
|
// whole point of a fragment. A typo'd $ref is otherwise invisible until a
|
|
// reader opens /api/docs.json and finds a dangling pointer.
|
|
const refs = JSON.stringify(spec.paths).match(/#[/]components[/]schemas[/]([A-Za-z0-9_]+)/g) || []
|
|
const shared = ['Error', 'ValidationError']
|
|
for (const name of new Set(refs.map((r) => r.split('/').pop()))) {
|
|
const resolvable = Object.hasOwn(spec.components.schemas, name) || shared.includes(name)
|
|
assert.ok(resolvable, `$ref to ${name} resolves to nothing — not defined here, not one of core's shared schemas`)
|
|
}
|
|
})
|
|
|
|
test('every path in the fragment is fully qualified', () => {
|
|
const spec = JSON.parse(fs.readFileSync(FRAGMENT, 'utf8'))
|
|
for (const p of Object.keys(spec.paths)) {
|
|
// §6.1a: core merges the fragment verbatim and never re-derives a prefix, so
|
|
// a router-relative path here is a path nothing serves.
|
|
assert.match(p, /^\/api\/v1\/(public|admin|player)\//, `${p} is not a fully-qualified URL`)
|
|
assert.doesNotMatch(p, /\/$/, `${p} has a trailing slash — no client calls that URL`)
|
|
}
|
|
})
|
|
|
|
test("the manifest and the module's declared mounts agree", () => {
|
|
const { routes } = JSON.parse(fs.readFileSync(MANIFEST, 'utf8'))
|
|
const { mounts } = JSON.parse(fs.readFileSync(path.join(__dirname, '..', '..', 'module.json'), 'utf8'))
|
|
|
|
const declared = []
|
|
for (const [tier, prefixes] of Object.entries(mounts)) {
|
|
for (const prefix of prefixes) declared.push(`/api/v1/${tier}${prefix}/`)
|
|
}
|
|
|
|
// Every route this module serves is under a prefix it declared. There is no
|
|
// exception here yet, and that is the point of asserting it now: phase 6 adds
|
|
// the `admin.users.detail` extension slot, whose routes live under core's
|
|
// `/api/v1/admin/users/` rather than under any mount of ours (§2.4). When that
|
|
// arrives this test must grow the exception deliberately, rather than a route
|
|
// outside every declared mount arriving unnoticed.
|
|
for (const route of routes) {
|
|
const under = declared.some((d) => route.path.startsWith(d))
|
|
assert.ok(under, `${route.method} ${route.path} is served from outside every mount module.json declares`)
|
|
}
|
|
})
|