`the manifest and the module's declared mounts agree` was written in phase 1 with its own exception named in a comment: when `admin.users.detail` arrives, its routes live on a resource core owns and the test must grow the exception deliberately rather than let a route outside every declared mount arrive unnoticed. This is that growth, and the test did its job — it failed on the first run after the slot was filled. A route is now legitimate if it is under a declared prefix OR under the mount of a slot `module.json` declares, and a declared slot that contributes no route fails too: core never checks that a declared slot was filled (`checkDeclared` covers `mounts` alone), so this is the only place an exception widening the check for nothing is noticed. Verified by pointing the slot mount at a path nothing serves and watching it fail. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PMH6bw1jXMgbyF3ZWGEzSM
207 lines
9.8 KiB
JavaScript
207 lines
9.8 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 manifest = JSON.parse(fs.readFileSync(path.join(__dirname, '..', '..', 'module.json'), 'utf8'))
|
|
|
|
const declared = []
|
|
for (const [tier, prefixes] of Object.entries(manifest.mounts)) {
|
|
for (const prefix of prefixes) declared.push(`/api/v1/${tier}${prefix}/`)
|
|
}
|
|
|
|
// **The exception this test predicted, now grown deliberately.** Phase 6 fills
|
|
// `admin.users.detail`, whose routes live on a resource CORE owns
|
|
// (`/api/v1/admin/users/:id`) rather than under any mount of ours — §2.4's
|
|
// fourth mount shape. So a route is legitimate if it is under a declared
|
|
// prefix, or under the mount of a slot this module declares.
|
|
//
|
|
// The slot's mount is restated here rather than imported, for the same reason
|
|
// the protocol catalogue is restated in `catalogue.test.js`: it is CORE's
|
|
// constant, and a module that derived it from its own generator would be
|
|
// checking that file against itself.
|
|
const SLOT_MOUNT = { 'admin.users.detail': '/api/v1/admin/users/' }
|
|
|
|
const slots = (manifest.extensions || []).map((slot) => {
|
|
const mount = SLOT_MOUNT[slot]
|
|
assert.ok(mount, `module.json declares slot "${slot}", which §2.4's table does not list`)
|
|
return { slot, mount }
|
|
})
|
|
|
|
const used = new Set()
|
|
|
|
for (const route of routes) {
|
|
if (declared.some((d) => route.path.startsWith(d))) continue
|
|
|
|
const slot = slots.find((s) => route.path.startsWith(s.mount))
|
|
assert.ok(
|
|
slot,
|
|
`${route.method} ${route.path} is served from outside every mount module.json declares, ` +
|
|
'and outside every slot it fills',
|
|
)
|
|
used.add(slot.slot)
|
|
}
|
|
|
|
// The other half, and the reason the exception is narrow: a declared slot that
|
|
// contributes no route is an exception widening this check for nothing. Core
|
|
// never checks that a declared slot was filled (`checkDeclared` covers `mounts`
|
|
// alone), so this is the only place it is noticed.
|
|
for (const { slot } of slots) {
|
|
assert.ok(used.has(slot), `module.json declares "${slot}" but no route in the manifest comes from it`)
|
|
}
|
|
})
|