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
This commit is contained in:
@@ -96,3 +96,66 @@ test('a setRuleset failure does not throw or stop the broadcast', async () => {
|
||||
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')
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user