Wire up the three uo-link sidecar surfaces that weren't integrated yet. Champion spawns - Ingest champ.update/champ.remove into a new shard_champs table (served from our own store, like online/houses); public /site/champs board with a nav link, live via the existing SSE feed (champ.* added to the public allowlist). Staff write plane (admin + moderator) - kick / ban / unban / broadcast via /admin/shard/*; actor is stamped server-side from the session, never the browser. Sidecar status codes mapped (403 disabled/ protected, 404 unknown, 503/504 transient). admin.audit events are logged and surfaced at /admin/shard/audit. - New admin "In-Game Ops" view (/admin/shard-ops), plus per-account Kick/Ban/Unban on the user-detail and character views (ShardAccountActions, self-gated to staff). Help-page (support) queue - Ingest page.new/updated/closed into a new shard_pages table; respond/close via /admin/shard/pages/*. Champ board and page queue are snapshotted from the sidecar's /champs and /pages on every WS (re)connect (guarded so a failed call never wipes local state). Verified live end-to-end against MariaDB + the Rust sidecar + ServUO; unit tests cover ingest routing (shardIngest.champsPages.test.js). Swagger regenerated. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0114TpmrNW4wNXsHq5CR72jQ
70 lines
3.2 KiB
JavaScript
70 lines
3.2 KiB
JavaScript
const { test, beforeEach } = require('node:test')
|
|
const assert = require('node:assert/strict')
|
|
|
|
const shardIngest = require('../src/utils/shardIngest')
|
|
|
|
// Build a set of stub deps that record the champ/page/state calls the dispatcher
|
|
// makes, plus a spy shardEvents.append and broadcast. Only the methods the tested
|
|
// kinds touch need to be real; the rest are no-op async so ingest() never throws.
|
|
function makeDeps() {
|
|
const calls = { champUpsert: [], champRemove: [], pageUpsert: [], pageRemove: [], appended: [], broadcast: [] }
|
|
const noop = async () => {}
|
|
return {
|
|
calls,
|
|
shardEvents: { append: async (row) => { calls.appended.push(row); return true } },
|
|
shardState: {
|
|
upsertChamp: async (ev) => { calls.champUpsert.push(ev) },
|
|
removeChamp: async (serial) => { calls.champRemove.push(serial) },
|
|
upsertPage: async (ev) => { calls.pageUpsert.push(ev) },
|
|
removePage: async (id) => { calls.pageRemove.push(id) },
|
|
// Unused by these kinds but present so any stray routing is a no-op.
|
|
clearOnline: noop, upsertOnline: noop, setOffline: noop, upsertHouse: noop, addEconomySample: noop,
|
|
},
|
|
uoLinkConfig: { recordStatus: noop },
|
|
broadcast: (ev) => { calls.broadcast.push(ev) },
|
|
log: { warn() {}, info() {}, error() {} },
|
|
}
|
|
}
|
|
|
|
beforeEach(() => shardIngest.reset())
|
|
|
|
test('champ.update routes to shardState.upsertChamp and is not written to the event log', async () => {
|
|
const deps = makeDeps()
|
|
const ev = { kind: 'champ.update', serial: '0x1', category: 'champion', name: 'Abyss', status: 'active', t: 1 }
|
|
const r = await shardIngest.ingest(ev, deps)
|
|
assert.equal(deps.calls.champUpsert.length, 1)
|
|
assert.equal(deps.calls.champUpsert[0].serial, '0x1')
|
|
assert.equal(r.logged, false) // champ.* is state-only, not appended to shard_events
|
|
assert.equal(deps.calls.appended.length, 0)
|
|
assert.equal(deps.calls.broadcast.length, 1) // still broadcast live
|
|
})
|
|
|
|
test('champ.remove routes to shardState.removeChamp', async () => {
|
|
const deps = makeDeps()
|
|
await shardIngest.ingest({ kind: 'champ.remove', serial: '0x2', t: 2 }, deps)
|
|
assert.deepEqual(deps.calls.champRemove, ['0x2'])
|
|
})
|
|
|
|
test('page.new and page.updated upsert the page; page.closed removes it', async () => {
|
|
const deps = makeDeps()
|
|
await shardIngest.ingest({ kind: 'page.new', pageId: '0x24C', type: 'Bug', sender: { name: 'Al' }, t: 3 }, deps)
|
|
await shardIngest.ingest({ kind: 'page.updated', pageId: '0x24C', handled: true, t: 4 }, deps)
|
|
await shardIngest.ingest({ kind: 'page.closed', pageId: '0x24C', t: 5 }, deps)
|
|
assert.equal(deps.calls.pageUpsert.length, 2)
|
|
assert.deepEqual(deps.calls.pageRemove, ['0x24C'])
|
|
})
|
|
|
|
test('admin.audit is appended to the event log (moderation history)', async () => {
|
|
const deps = makeDeps()
|
|
const r = await shardIngest.ingest({ kind: 'admin.audit', action: 'ban', actor: 'web:jane', target: 'griefer', t: 6 }, deps)
|
|
assert.equal(r.logged, true)
|
|
assert.equal(deps.calls.appended.length, 1)
|
|
assert.equal(deps.calls.appended[0].kind, 'admin.audit')
|
|
})
|
|
|
|
test('champ.remove without a serial is a harmless no-op', async () => {
|
|
const deps = makeDeps()
|
|
await shardIngest.ingest({ kind: 'champ.remove', t: 7 }, deps)
|
|
assert.deepEqual(deps.calls.champRemove, [undefined])
|
|
})
|