Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
36 changes: 36 additions & 0 deletions docs/test-audit.md
Original file line number Diff line number Diff line change
@@ -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 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.
8 changes: 8 additions & 0 deletions test/live/suite.js
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand All @@ -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 () => {
Expand Down Expand Up @@ -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]);
Expand Down
Loading