Merge remote-tracking branch 'origin/edge' into feat/spawn-atlas-parse

This commit is contained in:
2026-07-28 16:45:56 -05:00
17 changed files with 677 additions and 0 deletions

View File

@@ -56,6 +56,7 @@ const originals = {
listIdoc: shardState.listIdoc,
onlineCount: shardState.onlineCount,
latestEconomy: shardState.latestEconomy,
getRuleset: shardState.getRuleset,
getSafe: uoLinkConfig.getSafe,
}
afterEach(() => {
@@ -63,6 +64,7 @@ afterEach(() => {
shardState.listIdoc = originals.listIdoc
shardState.onlineCount = originals.onlineCount
shardState.latestEconomy = originals.latestEconomy
shardState.getRuleset = originals.getRuleset
uoLinkConfig.getSafe = originals.getSafe
})
@@ -243,6 +245,66 @@ test('getIdoc preserves Date columns rather than flattening them to {}', async (
assert.equal(res.body[0].updatedAt.toISOString(), when.toISOString())
})
// ── getRuleset: "never published" is a real answer ──────────────────────
test('getRuleset serves null when the shard has never published a ruleset', async () => {
shardState.getRuleset = async () => null
const res = mockRes()
await ctrl.getRuleset({ viewerLevel: 'anonymous' }, res)
// Deliberately null, not {} — the page says "not published yet" rather than
// rendering an empty ruleset as though the shard had no rules.
assert.equal(res.body, null)
assert.equal(res.statusCode, 200)
})
test('getRuleset serves the published ruleset whole, nested blocks intact', async () => {
shardState.getRuleset = async () => ({
kind: 'world.ruleset',
rev: '1a2b3c4d',
shard: 'UOMysticmoon',
expansion: 'EJ',
systems: { cityLoyalty: true, vvv: true, factions: false },
caps: { skill: 1000, totalSkill: 7000, stat: 225 },
champions: { powerScrolls: 6, rankThresholds: [5, 10, 13] },
})
const res = mockRes()
await ctrl.getRuleset({ viewerLevel: 'anonymous' }, res)
assert.equal(res.body.expansion, 'EJ')
assert.equal(res.body.systems.vvv, true)
assert.equal(res.body.caps.totalSkill, 7000)
// Arrays must survive projection as arrays, not become objects.
assert.deepEqual(res.body.champions.rankThresholds, [5, 10, 13])
})
// §3.6.1's rule: a read path that returns shard data and does not project is a
// bug. The ruleset frame carries no actor today, but it goes through the same
// gate — so a future block that does cannot leak.
test('getRuleset projects: acct/webId never survive below admin', async () => {
shardState.getRuleset = async () => ({
expansion: 'EJ',
connect: 'play.example.com,2593',
owner: { name: 'Lord British', acct: 'lb_acct', webId: 7 },
})
for (const level of ['anonymous', 'logged_in', 'player', 'staff']) {
const res = mockRes()
await ctrl.getRuleset({ viewerLevel: level }, res)
assert.equal(res.body.owner.acct, undefined, `${level} saw acct`)
assert.equal(res.body.owner.webId, undefined, `${level} saw webId`)
// `connect` defaults to the anonymous rung: an operator who published it
// meant it to be readable.
assert.equal(res.body.connect, 'play.example.com,2593')
}
})
test('getRuleset degrades to a 500 when the model fails, without throwing', async () => {
shardState.getRuleset = async () => {
throw new Error('pool down')
}
const res = mockRes()
await ctrl.getRuleset({ viewerLevel: 'anonymous' }, res)
assert.equal(res.statusCode, 500)
assert.equal(res.body.message, 'Internal Server Error')
})
// ── getStatus assembles the summary ─────────────────────────────────────
test('getStatus merges the sidecar config with the online count and latest economy', async () => {
uoLinkConfig.getSafe = async () => ({

View File

@@ -0,0 +1,98 @@
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)
})