module-rust, id 'rust', built from the Integration Kit's template. Phase 1's job
is the kit's own argument: get every seam working at once with almost nothing in
them, so that afterwards you break exactly one at a time.
What is here:
* /rust on all three tiers, because the loader holds module.json's mounts against
what is registered in BOTH directions -- so the declaration and the
registration land together or not at all. The player tier is honestly thin: it
answers the server list on the authenticated tier, delegating to the same model
the public tier uses so the two cannot drift while they are meant to be the
same. It is the address the app will call, registered now rather than moved
later.
* Two tables. rust_servers is configuration an operator writes; rust_server_state
is what a sidecar reported. Separate tables because they have different
writers, lifetimes and audiences -- and because purging observed state while
keeping the configuration is a thing an operator will want.
* Per-server sidecar tokens through ctx.secretBox, write-only in the API. The
admin list reports hasToken and never the credential, and an empty token on a
save leaves the stored one alone -- a form that posts its own blank field would
otherwise erase a credential every time somebody renamed a server.
* A real sidecar client. It never throws: every call answers {ok, status, data},
and the status is what tells a wrong URL from a wrong token from a mismatched
protocol -- all three present as 'the site says my server is offline' and each
has a different fix.
* The five guards, green: check:imports, check:swagger, check:externals, and both
suites.
What is deliberately NOT registered: the Team provider, triggers, audiences,
engagement seeds, notification streams, the four event catalogues, and the two
extension slots. Each arrives with the phase that has something real to put in
it, and a test asserts their absence so that removing it is deliberate. A
declared trigger nothing emits and a declared slot nothing fills are both
surfaces an operator can configure and then wait on, which is worse than an
absent one because the absence is visible.
Two corrections to the kit's template, both feedback for a later phase:
* registration.test.js read one page BY NAME to check declared slots are
rendered, so a module declaring none dies on ENOENT before reaching the loop
that would have been empty. It now scans every file under src/routes.
* test/_fakes.js supplied validator: {}. An admin router that builds validation
chains at file scope cannot be required with that, so the fake holds the real
express-validator -- for the same reason it holds a real express Router.
The kit was right about noGameConnection.test.js: its header predicts that a
module adding a sidecar client will see the check go red, names sidecarClient.js
as the file to allow, and says narrow it rather than delete it. That is exactly
what happened on the first run, and the fix was the one line the header names.
Installed into a real core and verified: the module reaches 'started', publishes
its capability, serves its chunk, and renders a server whose server.hello
originated in a live Rust server.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
150 lines
6.0 KiB
JavaScript
150 lines
6.0 KiB
JavaScript
// The boundary check, checked.
|
|
//
|
|
// `scripts/checkImports.js` is the acceptance test for the whole module contract
|
|
// (MODULE_API.md §5.1), and a check that has never been shown to fail is a check
|
|
// nobody knows the state of. These point it at fixtures that break each rule and
|
|
// assert it says so — and at prose that merely *describes* breaking them, which
|
|
// is what it got wrong the first time it was run.
|
|
//
|
|
// **Every fixture is a template literal, and that is load-bearing.** The scanner
|
|
// reads the files in this directory too, so an ordinary quoted string holding
|
|
// `require('../../x')` would make this file fail the very check it is testing.
|
|
// Templates are blanked by the stripper for exactly this class of text: source
|
|
// being composed as data is not source being imported.
|
|
|
|
const test = require('node:test')
|
|
const assert = require('node:assert')
|
|
const fs = require('node:fs')
|
|
const os = require('node:os')
|
|
const path = require('node:path')
|
|
|
|
const { scan, stripCommentsAndTemplates, SERVER_ROOT, MODULE_ROOT } = require('../scripts/checkImports')
|
|
|
|
/** Write `files` into a throwaway module tree and scan it. */
|
|
function scanFixture(files, { dev = new Set() } = {}) {
|
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'module-tpl-'))
|
|
const src = path.join(root, 'server')
|
|
for (const [name, source] of Object.entries(files)) {
|
|
const file = path.join(src, name)
|
|
fs.mkdirSync(path.dirname(file), { recursive: true })
|
|
fs.writeFileSync(file, source)
|
|
}
|
|
try {
|
|
return scan(src, root, { shipped: (f) => !f.startsWith(path.join(src, 'test') + path.sep), dev })
|
|
} finally {
|
|
fs.rmSync(root, { recursive: true, force: true })
|
|
}
|
|
}
|
|
|
|
test('the real server half is clean', () => {
|
|
assert.deepStrictEqual(scan(SERVER_ROOT, MODULE_ROOT), [])
|
|
})
|
|
|
|
test('catches a relative path that escapes the module root', () => {
|
|
const found = scanFixture({ 'a.js': `require('../../server/src/utils/db')` })
|
|
assert.strictEqual(found.length, 1)
|
|
assert.strictEqual(found[0].why, 'escapes the module root')
|
|
})
|
|
|
|
test('allows a relative path that stays inside it, however deep', () => {
|
|
assert.deepStrictEqual(
|
|
scanFixture({ 'deep/nested/a.js': `require('../../../module.json')` }),
|
|
[],
|
|
)
|
|
})
|
|
|
|
test('catches an absolute path', () => {
|
|
const found = scanFixture({ 'a.js': `require('/etc/passwd')` })
|
|
assert.strictEqual(found[0].why, 'absolute path')
|
|
})
|
|
|
|
test('catches a bare specifier in shipped code, even a devDependency', () => {
|
|
// The rule that makes the boundary real: express arrives on ctx. A shipped
|
|
// file requiring it would fail on a real install, because a module lives
|
|
// outside core's server/ and never reaches core's node_modules.
|
|
const found = scanFixture({ 'a.js': `const express = require('express')` }, { dev: new Set(['express']) })
|
|
assert.strictEqual(found.length, 1)
|
|
assert.match(found[0].why, /should this come from ctx/)
|
|
})
|
|
|
|
test('allows a devDependency in test code, which never runs inside core', () => {
|
|
assert.deepStrictEqual(
|
|
scanFixture({ 'test/a.js': `const express = require('express')` }, { dev: new Set(['express']) }),
|
|
[],
|
|
)
|
|
})
|
|
|
|
test('allows node builtins anywhere, with or without the node: prefix', () => {
|
|
assert.deepStrictEqual(
|
|
scanFixture({ 'a.js': `require('path'); require('node:fs'); import crypto from 'node:crypto'` }),
|
|
[],
|
|
)
|
|
})
|
|
|
|
test('allows node:test, which older Node versions omit from builtinModules', () => {
|
|
// The first CI run failed on exactly this and on nothing else: `builtinModules`
|
|
// omits `test` on Node 20 and includes it on Node 24, so every test file in
|
|
// this suite was reported as breaking the module boundary. The check asks
|
|
// Node (`isBuiltin`) rather than rebuilding the list, and treats the `node:`
|
|
// prefix as sufficient on its own — a prefixed specifier can never resolve to
|
|
// a package, whatever the running version enumerates.
|
|
assert.deepStrictEqual(
|
|
scanFixture({ 'a.js': `require('node:test'); require('node:test/reporters')` }),
|
|
[],
|
|
)
|
|
})
|
|
|
|
test('catches ESM and dynamic forms, not only require()', () => {
|
|
const found = scanFixture({
|
|
'a.js': [`import db from '../../core/db.js'`, `const x = await import('../../core/other.js')`].join('\n'),
|
|
})
|
|
assert.strictEqual(found.length, 2)
|
|
})
|
|
|
|
test('ignores a violation that is only DESCRIBED in a comment', () => {
|
|
// The first run of this check failed on its own documentation, and on
|
|
// index.js's comment explaining why the module must never require('express').
|
|
// Prose about the rule must not trip the rule.
|
|
assert.deepStrictEqual(
|
|
scanFixture({
|
|
'a.js': [
|
|
`// Never write require("../../server/src/utils/db") - it escapes the module root.`,
|
|
`/* Nor import express from "express": core hands it over on ctx. */`,
|
|
`const path = require('path')`,
|
|
].join('\n'),
|
|
}),
|
|
[],
|
|
)
|
|
})
|
|
|
|
test('ignores a specifier-shaped string inside a template literal', () => {
|
|
assert.deepStrictEqual(
|
|
scanFixture({ 'a.js': ['const sql = ', '`SELECT 1 -- require("../../x")`'].join('') }),
|
|
[],
|
|
)
|
|
})
|
|
|
|
test('a comment opener inside a string does not swallow the rest of the file', () => {
|
|
// The reason this is a character walk and not a regexp: a URL in a string
|
|
// contains `//`, and treating that as a comment would blank everything after
|
|
// it — turning the check into one that silently passes.
|
|
const found = scanFixture({
|
|
'a.js': [`const url = 'https://example.com/x'`, `require('../../escaped')`].join('\n'),
|
|
})
|
|
assert.strictEqual(found.length, 1, 'the specifier after a URL string was missed')
|
|
})
|
|
|
|
test('a quote inside a comment does not swallow the rest of the file', () => {
|
|
const found = scanFixture({
|
|
'a.js': [`// don't do this`, `require('../../escaped')`].join('\n'),
|
|
})
|
|
assert.strictEqual(found.length, 1)
|
|
})
|
|
|
|
test('stripping preserves line numbers', () => {
|
|
// Blanked rather than removed, so anything that later reports a line still
|
|
// reports the right one.
|
|
const src = ['/* a', 'b', 'c */', `require("x")`, ''].join('\n')
|
|
assert.strictEqual(stripCommentsAndTemplates(src).split('\n').length, src.split('\n').length)
|
|
})
|