22 test files moved from core, plus the two that were split out of files core keeps. 351 tests pass. One change runs through every moved test, and it is the boundary rather than a chore: core internals can no longer be stubbed by requiring them, because there are none to require. `../utils/db` and `../model/settings` do not exist here. What a test controls instead is the ctx core would have handed over, installed once by test/_setup.js -- which is a better seam anyway, since it is exactly the surface the contract promises and nothing wider. The ctx _setup installs is deliberately unfrozen. Core freezes what it hands a module and entry.test.js still asserts against a frozen one; but a test that needs settings.get to return a path has to be able to say so. Two tests changed SHAPE, and that is the boundary too. fromShardEvent used to assert through publish() into pushDevices and a captured fetch -- which endpoints were hit, how many requests went out. None of that is this module's any more: publish is ctx.push.publish, and the device registry and the relay are behind it. Reaching for them from here would be reaching past ctx. What remains is what the module owns and is the part worth guarding: a game account resolves to a website user, a personal target that resolves to nobody is dropped rather than published, and a sensitive kind never reaches publish at all. Co-Authored-By: Claude <noreply@anthropic.com>
162 lines
6.8 KiB
JavaScript
162 lines
6.8 KiB
JavaScript
const { test, beforeEach } = require('node:test')
|
|
const assert = require('node:assert/strict')
|
|
|
|
const shardIngest = require('../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')
|
|
})
|