From 3daa8dd6ad6c15c791840ac3940e2cdc1ee48716 Mon Sep 17 00:00:00 2001 From: Michael Gilardi <11632652+mgilardi@users.noreply.github.com> Date: Mon, 9 Nov 2026 04:05:48 -0700 Subject: [PATCH 1/2] Add the first test audit, and two checks it found missing The audit maps every README promise to its test. Two were untested: that blocked text never reaches the server log, and that an admin joining a server with a broken config is told. Both now have live checks, which passed on throwaway Paper 26.2 and 26.3 (61/61, 2 real-client skips). The README status line is brought up to date with the 0.2.0 tests. Co-Authored-By: Claude Opus 5.5 --- README.md | 2 +- docs/test-audit.md | 36 ++++++++++++++++++++++++++++++++++++ test/live/suite.js | 8 ++++++++ 3 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 docs/test-audit.md diff --git a/README.md b/README.md index f0d5045..3882b5f 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Chat rules for family and community Paper servers. Players hear people in the same world, plus anyone who shares a "circle" with them, so groups such as families or age groups keep their own conversations across every world. -**Status:** pre-release. It has passed its acceptance tests on throwaway Paper 26.2 and 26.3 servers, using protocol bots with LuckPerms. That covers chat and private messages for every pair of players, replies, blocked commands, the word filter and its bypass, books, `/circles`, reloads and 20 bots chatting at once. Signs and anvil renames weren't tested, and the BlueMapHUD share was tested with a stand-in plugin here, then with the real BlueMapHUD 0.1.0 and HouseRules 0.1.0 jars together on a throwaway Paper 26.2 server, with bots and simple permissions rather than LuckPerms. It hasn't been seen in a real game client yet, so the wording and colours haven't been checked by a person. See [the live tests](test/live/README.md). +**Status:** pre-release. It passes its acceptance tests on throwaway Paper 26.2 and 26.3 servers, using protocol bots with LuckPerms: chat and private messages for every pair of players, replies, blocked commands, the word filter and its bypass, books, `/circles`, reloads, a broken config, and 20 bots chatting at once. BlueMinimap's share was tested with a stand-in plugin here, and with the real BlueMinimap 0.2.0 and HouseRules 0.2.0 jars together on Paper 26.1.2, 26.2 and 26.3. Signs and anvil renames weren't tested. It hasn't been seen in a real game client yet, so the wording and colours haven't been checked by a person. See [the live tests](test/live/README.md) and the [test audit](docs/test-audit.md). - Requires Paper 26.1 or newer and Java 25. Paper 1.21.11 and older aren't supported. - With no circles set up it changes nothing about who hears whom, so it's safe to install first and configure after. diff --git a/docs/test-audit.md b/docs/test-audit.md new file mode 100644 index 0000000..ebf893a --- /dev/null +++ b/docs/test-audit.md @@ -0,0 +1,36 @@ +# Test audit + +Kept by this repo's owning session (Minecraft: Plugins Lead), per `~/Code/CLAUDE.md`, "Test self-audits". Newest first. + +## 2026-11-09: first full audit (at 0.2.0) + +Each promise in the README, and the test that holds it to it. "Live" is `test/live/suite.js` on throwaway Paper 26.2 and 26.3 with bots and LuckPerms; "unit" is `src/test`. + +| Promise | Covered by | +| --- | --- | +| No circles: nothing changes | unit `noCirclesMeansVanilla`, `noConfigIsVanillaChat`; live "No circles: vanilla chat", "starts with no config as vanilla chat" | +| Same world group reaches, unless circle-only; shared circle reaches anywhere | unit `sameWorldGroupReaches`, `sharedCircleReachesAcrossWorlds`, `circleOnlyWorld`, `worldGroups`, `reachIsSymmetric`; live reach matrix, every pair | +| No circle in a circle-only world: can't chat, message or write, and is told why | live "N2 (hall, no circle) is told…", book edits, `/circles` explanation; signs need a real client (skipped) | +| Private messages out of reach aren't sent; told who instead; selectors refused | live private-message matrix (every command), "/msg with a selector is refused" | +| `/r` re-checked on reply | unit `replies`; live "Replies" | +| `/me` and `/teammsg` off while circles are in use; bypass keeps them | unit `broadcastCommandsOffWhileCirclesAreOn`; live "Blocked and broadcast commands" | +| BlueMinimap shares follow chat; old BlueMapHUD event for one release | live share section with `HR_SHARE_PLUGIN=new` and `old`; BlueMinimap's `test/live/houserules.js` with the real jars | +| Blocked commands in every form, own message | unit `blockedCommandsInEveryForm`; live | +| Filter: whole words, disguises, allow list; Scunthorpe passes | unit `wholeWordsOnly`, `seesThroughDisguises`, `allowListWins`, `phrases`, `defaultList*`; live | +| **Blocked text is never logged or stored** | **new** live "filtered words never reach the server log" (chat and `/msg`) | +| Filter bypass, granted and removed without a relog; others still filtered | unit `filterBypass*`; live | +| `houserules.bypass` and `.filter.bypass` off for everyone, ops included | unit `bypassesAreOffForEveryoneOpsIncluded` | +| Nobody-heard notes: all before any repeat, cooldown | unit `everyNoteBeforeAnyRepeatAndNeverTwiceInARow`, `cooldown`, `emptyIsQuiet`; live | +| Permission changes take effect without a relog | live "Circles granted and removed with LuckPerms, no relog" | +| Broken config at startup pauses chat and messages; console says why | unit `pausedBlocksMessagesButNotOtherCommands`; live | +| **Admins are told about a broken config when they join** | **new** live "an admin joining is told…", and "a player who isn't an admin isn't" | +| A broken reload keeps the working rules | unit `brokenYamlIsAnErrorWithNoSettings`; live | +| `/circles`, names only with permission | live `/circles` section | +| `/houserules check` explains | live | +| 20 players chatting at once stay quick | live "Load" (TPS, 95th percentile under 500 ms) | + +**Gaps added this audit:** the two in bold. Both passed on 26.2 and 26.3 (61/61, 2 real-client skips). + +**Skipped, with reasons:** +- Sign text and anvil renames: bots can't place and edit signs or use an anvil. They're on the real-client list. +- `ReachService` "safe from any thread": it reads the same immutable rules snapshot chat uses. A concurrency test would be overkill for a read of an immutable object; the live load test exercises it from the async chat thread. diff --git a/test/live/suite.js b/test/live/suite.js index c626540..4da13ec 100644 --- a/test/live/suite.js +++ b/test/live/suite.js @@ -326,7 +326,11 @@ async function pausedOnBrokenStart() { s.writeConfig('config.yml', 'circles: [red\nnope: : :'); await s.start(); check('the console explains the pause', /chat and private messages are paused until it's fixed/.test(s.log)); + s.cmd('op R2'); await sleep(300); await join(['R1', 'R2']); + await sleep(800); + check('an admin joining is told chat is paused, and how to fix it', bots.R2.lines(0).some((l) => /paused because the config couldn't be read/.test(l)), bots.R2.lines(0).slice(-3).join(' | ')); + check('a player who isn\'t an admin isn\'t', !bots.R1.lines(0).some((l) => /paused because the config couldn't be read/.test(l))); const m1 = bots.R1.mark(), m2 = bots.R2.mark(); bots.R1.say('paused hello'); await sleep(1200); check('chat is paused, and the player is told', bots.R1.heard('Chat is paused for a moment', m1) && !bots.R2.heard('paused hello', m2)); @@ -336,6 +340,7 @@ async function pausedOnBrokenStart() { await reload(); const k = bots.R2.mark(); bots.R1.say('back again'); await sleep(1200); check('fixing the config and reloading brings chat back', bots.R2.heard('back again', k)); + s.cmd('deop R2'); } (async () => { @@ -372,6 +377,9 @@ async function pausedOnBrokenStart() { await pausedOnBrokenStart(); section('Server log'); + // README: "Blocked text is never logged or stored." The words the filter test used must not be in the server log. + const leaked = ['sh1t', 'b1tch'].filter((w) => s.log.toLowerCase().includes(w)); + check('filtered words never reach the server log', leaked.length === 0, leaked.join(', ')); const errors = s.errors().filter((l) => !/config.yml: isn't valid YAML|couldn't read its config|The config has problems|SEVERE.*HouseRules.*(YAML|paused|kept)/.test(l)); check('no errors or exceptions in the log', errors.length === 0, errors.slice(0, 8).join(' || ')); check('no test player died and respawned elsewhere', !/HRT death /.test(s.log), (s.log.match(/.*HRT death .*/) || [''])[0]); From aa16d18f3e0415d82e55612c9f1c339997c38ef6 Mon Sep 17 00:00:00 2001 From: Michael Gilardi <11632652+mgilardi@users.noreply.github.com> Date: Mon, 9 Nov 2026 04:06:06 -0700 Subject: [PATCH 2/2] Describe the ReachService threading exactly Co-Authored-By: Claude Opus 5.5 --- docs/test-audit.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/test-audit.md b/docs/test-audit.md index ebf893a..a04cf57 100644 --- a/docs/test-audit.md +++ b/docs/test-audit.md @@ -33,4 +33,4 @@ Each promise in the README, and the test that holds it to it. "Live" is `test/li **Skipped, with reasons:** - Sign text and anvil renames: bots can't place and edit signs or use an anvil. They're on the real-client list. -- `ReachService` "safe from any thread": it reads the same immutable rules snapshot chat uses. A concurrency test would be overkill for a read of an immutable object; the live load test exercises it from the async chat thread. +- `ReachService` "safe from any thread": it reads the rules from one snapshot that a reload swaps in whole (a `volatile` field with `final` contents), plus the two players' own permissions and world, the same reads async chat already makes. A dedicated concurrency test would be overkill; the live load test exercises the same path from the async chat thread with 20 bots.