fix(kit): everything the acceptance run found — Phase 5 slice 3
A cold agent was given this repo and the documents it links to, and nothing else — no core source, no module-uo — and asked to build a module for a second game. It did, in one pass. The record is docs/modules/kit-acceptance.md; this is the repair list, plus the two things it recommended that were not defects. The one it could not find, because it had no core to render against: a module page built exactly as this kit teaches renders OUTSIDE the site. PublicLayout is the chrome, not the body. Core grew an opt-in `shell` prop for it (MODULE_API_VERSION 1.5.0, website#148); the template passes shell="narrow" and chapter 2 explains why you name a width and never a class. Fixed: - **F1, and the worst of them, because it lands in the first twenty minutes.** `npm run check:swagger` failed on a PRISTINE template on Windows: the check compared the committed fragment byte-for-byte and a default Windows clone is CRLF while the generator writes LF. The message blamed "the routes or their annotations". Now `template/.gitattributes` pins `eol=lf` and the comparison normalises line endings anyway — a check may only fail for the reason it names, and this one names a diagnosis. - **F3** — `.gitea/workflows/release.yml` carries `gitea.example.com` and `your-org/your-module` under a literal `# CHANGE THESE`, was not in the rename checklist, and `checkRenameSites.js` could not match it, so CI was silent by construction. Row added, pattern widened. (The agent reported both workflow flavours; only the Gitea one is affected — GitHub supplies its own variables. Corrected in the record.) The near-miss is kept in the check's comments and its suite: the obvious widening is `example\.com`, which fires on a fixture URL in checkImports.test.js. Every alternative has to be a string that cannot occur by accident, which is the same rule that made the id `examplegame`. - **F4** — the release bundle's include list was hardcoded, so adding `server/utils/` would have silently dropped it from every release while the bundle check stayed green. Inverted to an exclusion list, in both flavours, and run by hand because a release workflow never executes in CI. - **F5** — the annotation-quoting warning was wrong in both directions, and the correction is measured rather than reasoned. A backtick is harmless (the template's own description has two spans and they survive). A `"` is not, and it does not throw: `'A "quoted" status'` is silently TRUNCATED to `A "` while swagger-autogen prints Success and the error capture sees nothing. The only signal is check:swagger blaming your routes. - **F6** — `template/.gitignore`, so a copied template that is `git init`ed inherits ignore rules instead of nothing. - **F7** — the UI kit is eight exports across five rows, not seven. The contract said seven and this kit had faithfully carried the miscount out of it. Adopted, not defects: - Chapter 1 now says to run every check on the untouched copy first. That is what found F1; without a baseline the first failure is ambiguous forever. - The template ships the §2.7 self-check the agent wrote for itself. The rule has no CI in general — an outbound socket is not statically detectable — but a module can make a decidable claim about its own tree. Ported from its code with a header explaining how to NARROW it when a sidecar client arrives, since talking to your sidecar is the expected shape and is not what §2.7 forbids. The pin moves to website edge 4ad8b2b, the 1.5.0 bump, and template/module.json declares ^1.5.0 — so checkCoreApi's equality assertion still holds and the template uses a member that exists only at that ref and later. 32 server + 18 client template tests, 21 kit-script tests, all four checks green. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
124
template/server/test/noGameConnection.test.js
Normal file
124
template/server/test/noGameConnection.test.js
Normal file
@@ -0,0 +1,124 @@
|
||||
// ── §2.7's last rule, given the CI it does not have ───────────────────────
|
||||
//
|
||||
// `book/02-website-module.md` is explicit that "the website process never opens a
|
||||
// connection to a game server" is the **one boundary rule with no CI behind it**:
|
||||
// an outbound socket is not statically detectable the way an internal `require`
|
||||
// is, so in general the rule is held up by review and by understanding it.
|
||||
//
|
||||
// True of the general case, and not a reason to check nothing. A module can state
|
||||
// a narrower, completely decidable property about **itself**, and this one says:
|
||||
// the shipped server half references no networking primitive at all. Everything
|
||||
// it knows arrives from its own tables, which its sidecar writes.
|
||||
//
|
||||
// Adopted from the kit's acceptance run (`docs/modules/kit-acceptance.md`), where
|
||||
// a reader building a Rust module wrote it unprompted after reading that the rule
|
||||
// had no CI — and observed that for Rust in particular, which ships RCON over
|
||||
// WebSocket, `new WebSocket(rconUrl)` in `boot.js` is about ten lines away.
|
||||
//
|
||||
// ── WHEN YOU ADD A SIDECAR CLIENT, NARROW THIS. DO NOT DELETE IT. ─────────
|
||||
//
|
||||
// Talking to *your sidecar* over HTTP is the expected shape and is not what §2.7
|
||||
// forbids — the rule is about the **game server**. So the moment your module
|
||||
// grows, say, `server/sidecarClient.js`, this test starts failing correctly and
|
||||
// the fix is to allow that one file:
|
||||
//
|
||||
// const MAY_OPEN_SOCKETS = new Set(['sidecarClient.js'])
|
||||
//
|
||||
// and keep the rest of the tree under the ban. What you get for that is a test
|
||||
// that names the *one* file allowed to reach the network — which is exactly the
|
||||
// file a reviewer should be reading closely, and exactly the place a game-server
|
||||
// URL would appear if the rule were ever broken.
|
||||
//
|
||||
// Scope: SHIPPED code only. `test/` and `scripts/` never run inside core's process.
|
||||
|
||||
const test = require('node:test')
|
||||
const assert = require('node:assert')
|
||||
const fs = require('node:fs')
|
||||
const path = require('node:path')
|
||||
|
||||
const SERVER_ROOT = path.resolve(__dirname, '..')
|
||||
const NOT_SHIPPED = new Set(['test', 'scripts', 'node_modules', 'swagger'])
|
||||
|
||||
/** Every shipped `.js` file under `server/`. */
|
||||
function shippedFiles(dir = SERVER_ROOT, out = []) {
|
||||
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||
if (entry.isDirectory()) {
|
||||
if (dir === SERVER_ROOT && NOT_SHIPPED.has(entry.name)) continue
|
||||
if (entry.name === 'node_modules') continue
|
||||
shippedFiles(path.join(dir, entry.name), out)
|
||||
} else if (entry.isFile() && entry.name.endsWith('.js')) {
|
||||
out.push(path.join(dir, entry.name))
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
/**
|
||||
* Blank comments, so prose ABOUT the rule does not trip the rule.
|
||||
*
|
||||
* This file is itself the proof that it is needed: the paragraphs above say
|
||||
* "WebSocket" several times. `scripts/checkImports.js` documents hitting exactly
|
||||
* this on its own documentation, and it is the third time in this project's
|
||||
* history that a boundary check has failed on the text explaining it.
|
||||
*
|
||||
* Blanked rather than deleted, so line numbers in a failure still point at the
|
||||
* right line.
|
||||
*/
|
||||
function stripComments(src) {
|
||||
return src
|
||||
.replace(/\/\*[\s\S]*?\*\//g, (m) => m.replace(/[^\n]/g, ' '))
|
||||
.replace(/^[ \t]*\/\/.*$/gm, '')
|
||||
}
|
||||
|
||||
// Each is a way a Node process opens a socket. Matched as identifiers, so a
|
||||
// column named `websocket_url` inside a SQL string would not fire.
|
||||
const NETWORKING = [
|
||||
/\brequire\(\s*['"](?:node:)?(?:net|tls|dgram|http|https|http2)['"]\s*\)/,
|
||||
/\bfrom\s+['"](?:node:)?(?:net|tls|dgram|http|https|http2)['"]/,
|
||||
/\brequire\(\s*['"](?:ws|socket\.io-client|undici|axios|node-fetch|got)['"]\s*\)/,
|
||||
/\bnew\s+WebSocket\b/,
|
||||
/\bfetch\s*\(/,
|
||||
/\bXMLHttpRequest\b/,
|
||||
/\bEventSource\b/,
|
||||
]
|
||||
|
||||
test('no shipped file references a networking primitive (§2.7)', () => {
|
||||
const offenders = []
|
||||
for (const file of shippedFiles()) {
|
||||
const code = stripComments(fs.readFileSync(file, 'utf8'))
|
||||
for (const pattern of NETWORKING) {
|
||||
if (pattern.test(code)) {
|
||||
offenders.push(`${path.relative(SERVER_ROOT, file)} matches ${pattern}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
assert.deepStrictEqual(
|
||||
offenders,
|
||||
[],
|
||||
'the website process must never open a connection to a game server. If this is ' +
|
||||
'your sidecar client, allow that one file rather than removing the check — see ' +
|
||||
`the header of this file.\n ${offenders.join('\n ')}`,
|
||||
)
|
||||
})
|
||||
|
||||
test('the check can actually fail — it is pointed at a real violation', () => {
|
||||
// A check that has never been shown to fail is a check nobody knows the state
|
||||
// of. This is the game-server dial the rule exists to stop.
|
||||
const violation = "const socket = new WebSocket('ws://10.0.0.5:28016/' + rconPassword)"
|
||||
assert.ok(
|
||||
NETWORKING.some((p) => p.test(stripComments(violation))),
|
||||
'the guard would not have caught a direct game-server dial',
|
||||
)
|
||||
})
|
||||
|
||||
test('prose describing the rule does not trip it', () => {
|
||||
const prose = [
|
||||
'// A game shipping RCON over WebSocket means a module COULD write',
|
||||
"// const s = new WebSocket(url); require('net')",
|
||||
'// in about ten lines. It must not.',
|
||||
'const x = 1',
|
||||
].join('\n')
|
||||
for (const pattern of NETWORKING) {
|
||||
assert.ok(!pattern.test(stripComments(prose)), `${pattern} fired on a comment`)
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user