Files
website/server/test/shardIngest.ruleset.test.js
wtclaude 01a559792c fix(shard): answer with the instance name when the shard is unnamed
ServUO ships Server.cfg with `Name=My Shard`. An operator who never edited it
publishes that verbatim, so the rules page read "My Shard" under a header
carrying the real name. That value is the shard saying *unnamed* rather than
naming anything, so the site now answers with its own.

`settings.getInstanceName()` resolves `site_title || BRAND_NAME` — the same
resolution `getPublic().brand.name` already uses, so an install that set only
the site title can never show two different names on two pages. Bare
`brand.name` would have been wrong for exactly that case.

Substituted at INGEST rather than on read: world.ruleset is also broadcast
live, and the same object is handed to the SSE fan-out, so a read-time fix
would be undone by the next reconnect's frame. Matched case- and
padding-insensitively but only as a whole value, so a shard genuinely called
"My Shard Reborn" keeps its name.

Fixes a second ruleset writer found on the way: uoLinkSocket.backfill() called
shardState.setRuleset directly instead of going through the dispatcher as
ingestEach does, so the boot/reconnect snapshot silently skipped this
normalization. The two arrival orders have to produce the same stored frame.

Also renders a placeholder row on an unscored leaderboard — the instance name
with an em dash where a score goes, deliberately not shaped like an entry (no
medal, no bar) because a placeholder that looked like a real standing would be
a fabricated one. Presentation only; the API still sends an empty `top`.

Verified live against the shard + sidecar: rules page and leaderboards on web
and Android both correct.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U7CBg11prhLimL9iHSX1bP
2026-08-01 00:58:21 -05:00

162 lines
6.9 KiB
JavaScript

const { test, beforeEach } = require('node:test')
const assert = require('node:assert/strict')
const shardIngest = require('../src/utils/shardIngest')
// Protocol 3.0 world.ruleset routing. Same shape as shardIngest.protocol2.test.js:
// stubbed deps, asserting where the dispatcher sends the frame and whether it is
// appended to the event log.
function makeDeps() {
const calls = { rulesetSet: [], appended: [], broadcast: [] }
const noop = async () => {}
return {
calls,
shardEvents: { append: async (row) => { calls.appended.push(row); return true } },
shardState: {
setRuleset: async (ev) => { calls.rulesetSet.push(ev) },
// Present so any stray routing is a harmless no-op.
clearOnline: noop, upsertOnline: noop, setOffline: noop, upsertHouse: noop,
addEconomySample: noop,
},
shardLinks: { removeByAccount: noop },
uoLinkConfig: { recordStatus: noop },
broadcast: (ev) => { calls.broadcast.push(ev) },
pushDispatch: async () => {},
log: { warn() {}, info() {}, error() {} },
}
}
const FRAME = {
kind: 'world.ruleset',
t: 1000,
rev: '1a2b3c4d',
shard: 'UOMysticmoon',
expansion: 'EJ',
systems: { cityLoyalty: true, vvv: true, factions: false },
caps: { skill: 1000, totalSkill: 7000 },
}
beforeEach(() => shardIngest.reset())
test('world.ruleset routes to setRuleset with the whole frame', async () => {
const deps = makeDeps()
await shardIngest.ingest(FRAME, deps)
assert.equal(deps.calls.rulesetSet.length, 1)
const stored = deps.calls.rulesetSet[0]
assert.equal(stored.rev, '1a2b3c4d')
assert.equal(stored.expansion, 'EJ')
// The nested blocks must survive intact — the read model serves the frame whole.
assert.equal(stored.systems.vvv, true)
assert.equal(stored.caps.totalSkill, 7000)
})
// The shard re-emits world.ruleset on EVERY sidecar connect. Logging it would put
// a duplicate row in shard_events per reconnect, and server.hello already marks
// each of those — so this assertion is the guard on that decision.
test('world.ruleset is NOT appended to the event log', async () => {
const deps = makeDeps()
const r = await shardIngest.ingest(FRAME, deps)
assert.equal(r.logged, false)
assert.equal(deps.calls.appended.length, 0)
assert.equal(shardIngest.LOGGED_KINDS.has('world.ruleset'), false)
})
test('world.ruleset is broadcast (the rules page updates live)', async () => {
const deps = makeDeps()
await shardIngest.ingest(FRAME, deps)
assert.equal(deps.calls.broadcast.length, 1)
assert.equal(deps.calls.broadcast[0].kind, 'world.ruleset')
})
// A backfill replay must reach the store but must NOT re-animate the live ticker.
test('a backfilled world.ruleset still stores but does not broadcast', async () => {
const deps = makeDeps()
await shardIngest.ingest(FRAME, { ...deps, fromBackfill: true })
assert.equal(deps.calls.rulesetSet.length, 1)
assert.equal(deps.calls.broadcast.length, 0)
})
// A re-emitted identical ruleset is an overwrite, not an append: two ingests of
// the same rev leave one stored frame's worth of state, never a growing log.
test('a repeated world.ruleset overwrites rather than accumulating', async () => {
const deps = makeDeps()
await shardIngest.ingest(FRAME, deps)
await shardIngest.ingest({ ...FRAME, t: 2000 }, deps)
assert.equal(deps.calls.appended.length, 0)
assert.equal(deps.calls.rulesetSet.length, 2) // two writes...
assert.equal(deps.calls.rulesetSet[1].rev, '1a2b3c4d') // ...of the same singleton
})
// A model write that throws must not kill the feed — ingest swallows it and the
// frame is still broadcast.
test('a setRuleset failure does not throw or stop the broadcast', async () => {
const deps = makeDeps()
deps.shardState.setRuleset = async () => { throw new Error('db down') }
const r = await shardIngest.ingest(FRAME, deps)
assert.equal(r.logged, false)
assert.equal(deps.calls.broadcast.length, 1)
})
// ── Shard name fallback ────────────────────────────────────────────────────
//
// ServUO ships Server.cfg with `Name=My Shard`. An operator who never edited it
// publishes that verbatim, which says "unnamed" rather than naming anything — so
// the site answers with its own instance name instead of printing the stock
// default under a header carrying the real one.
//
// Applied at INGEST, not on read, because world.ruleset is also broadcast live:
// the same object goes to the SSE fan-out, so a read-time fix would be undone by
// the next reconnect's frame. These tests assert both halves.
function withSettings(deps, name) {
return { ...deps, settings: { getInstanceName: async () => name } }
}
test('the stock ServUO shard name is replaced with the instance name', async () => {
const deps = makeDeps()
await shardIngest.ingest({ ...FRAME, shard: 'My Shard' }, withSettings(deps, 'UOMysticmoon'))
assert.equal(deps.calls.rulesetSet[0].shard, 'UOMysticmoon')
})
test('the substituted name reaches the live broadcast, not just the store', async () => {
const deps = makeDeps()
await shardIngest.ingest({ ...FRAME, shard: 'My Shard' }, withSettings(deps, 'UOMysticmoon'))
assert.equal(deps.calls.broadcast.length, 1)
assert.equal(deps.calls.broadcast[0].shard, 'UOMysticmoon')
})
test('a missing or blank shard name gets the same treatment', async () => {
for (const shard of [undefined, null, '', ' ']) {
const deps = makeDeps()
await shardIngest.ingest({ ...FRAME, shard }, withSettings(deps, 'UOMysticmoon'))
assert.equal(deps.calls.rulesetSet[0].shard, 'UOMysticmoon')
}
})
// The match is on the whole value, case- and padding-insensitive. A shard that
// deliberately calls itself "My Shard Reborn" has named itself and keeps it.
test('a real name that merely contains the stock one is left alone', async () => {
const deps = makeDeps()
await shardIngest.ingest({ ...FRAME, shard: 'My Shard Reborn' }, withSettings(deps, 'UOMysticmoon'))
assert.equal(deps.calls.rulesetSet[0].shard, 'My Shard Reborn')
const padded = makeDeps()
await shardIngest.ingest({ ...FRAME, shard: ' MY SHARD ' }, withSettings(padded, 'UOMysticmoon'))
assert.equal(padded.calls.rulesetSet[0].shard, 'UOMysticmoon')
})
test('a shard that named itself is never overridden by the brand', async () => {
const deps = makeDeps()
await shardIngest.ingest(FRAME, withSettings(deps, 'Some Other Brand'))
assert.equal(deps.calls.rulesetSet[0].shard, 'UOMysticmoon')
})
// The settings read is a DB call on a path that must never fail ingest.
test('a settings read failure leaves the frame storable', async () => {
const deps = makeDeps()
const boom = { ...deps, settings: { getInstanceName: async () => { throw new Error('db down') } } }
await shardIngest.ingest({ ...FRAME, shard: 'My Shard' }, boom)
assert.equal(deps.calls.rulesetSet.length, 1)
assert.equal(deps.calls.rulesetSet[0].shard, 'My Shard')
})