test(server): port core's UO suite onto the ctx harness
All checks were successful
PR Checks / client-build (pull_request) Successful in 17s
PR Checks / server-tests (pull_request) Successful in 8m47s

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>
This commit is contained in:
2026-08-11 12:07:15 -05:00
committed by Claude
parent 740a677f92
commit 6b99d7e220
28 changed files with 4692 additions and 24 deletions

View File

@@ -0,0 +1,263 @@
// Ported from core in Phase 3 (MODULE_SYSTEM.md §2.7.1). One change runs through
// every moved test: 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 the seam the
// contract actually promises.
// Point the DB at a closed port BEFORE requiring anything that builds a pool.
// Nothing here reaches the database: these are the model's PURE parts — the
// flatten/shape rules the frame passes through on the way in and out — plus the
// visibility projection over the shapes they produce.
const { test, after } = require('node:test')
const assert = require('node:assert/strict')
const market = require('../model/shardMarket/shardMarket.model')
const clilocs = require('../model/shardClilocs/shardClilocs.model')
const clilocDb = require('../model/shardClilocs/shardClilocs.db')
const visibility = require('../utils/shardVisibility')
// Stand in for the cliloc table. Without this each unresolved lookup waits out
// the pool's 10s acquire timeout against the dead port — the model swallows the
// failure exactly as it would in production (an operator who never converted a
// cliloc file is in a supported state), so the RESULT is the same either way;
// this only stops the suite spending half a minute proving it.
const TABLE = new Map([[1023721, 'quarter staff']])
clilocDb.lookup = async (numbers) =>
numbers.filter((n) => TABLE.has(n)).map((n) => ({ number: n, text: TABLE.get(n) }))
const FRAME = {
kind: 'vendor.listing',
t: 1000,
serial: '0x40001234',
shopName: "Darrow's Bargains",
ownerSerial: '0x1A2B',
ownerName: 'Darrow',
location: { map: 'Trammel', x: 1421, y: 1699, z: 0, region: 'Britain', house: "Darrow's Villa" },
count: 2,
total: 2,
truncated: false,
items: [],
}
// ── flattenFrame ───────────────────────────────────────────────────────────
test('flattenFrame lifts the nested location into columns', () => {
const v = market.flattenFrame(FRAME)
assert.equal(v.serial, '0x40001234')
assert.equal(v.map, 'Trammel')
assert.equal(v.x, 1421)
assert.equal(v.region, 'Britain')
assert.equal(v.house, "Darrow's Villa")
})
// A vendor standing in the street has no house, and a frame from an older plugin
// may have no location at all. Neither is an error.
test('flattenFrame tolerates a missing location entirely', () => {
const v = market.flattenFrame({ serial: '0x1', shopName: null })
assert.equal(v.map, null)
assert.equal(v.x, null)
assert.equal(v.region, null)
assert.equal(v.house, null)
})
// `total` is what the SHOP holds; `count` is what the frame carried. A truncated
// shop must not report its published slice as its size, or the page says
// "showing 250 of 250" for a vendor holding three thousand stacks.
test('flattenFrame keeps the shop total separate from the published count', () => {
const v = market.flattenFrame({ ...FRAME, count: 250, total: 3104, truncated: true })
assert.equal(v.itemTotal, 3104)
assert.equal(v.truncated, true)
})
// An older plugin sends no `total`. Falling back to `count` is right — it is the
// only number available and it is correct whenever nothing was truncated.
test('flattenFrame falls back to count when total is absent', () => {
const v = market.flattenFrame({ ...FRAME, count: 7, total: undefined })
assert.equal(v.itemTotal, 7)
})
test('flattenFrame clips over-length strings rather than letting the insert fail', () => {
const v = market.flattenFrame({ ...FRAME, ownerName: 'x'.repeat(200) })
assert.equal(v.ownerName.length, 64)
})
// ── shapeItems ─────────────────────────────────────────────────────────────
//
// resolveMany never throws and, with no cliloc table reachable, resolves nothing
// — which is exactly the state of a shard whose operator never converted one, so
// these run against the real function rather than a stub.
test('shapeItems prefers the item\'s literal name over its cliloc', async () => {
const items = await market.shapeItems({
items: [{ serial: '0x1', itemId: 3922, price: 100, name: 'a shard sigil', cliloc: 1023721 }],
})
assert.equal(items[0].displayName, 'a shard sigil')
// The cliloc is kept regardless, so a later import can still re-resolve it.
assert.equal(items[0].cliloc, 1023721)
})
test('shapeItems resolves the cliloc when the item has no literal name', async () => {
const items = await market.shapeItems({
items: [{ serial: '0x1', itemId: 3922, price: 100, name: null, cliloc: 1023721 }],
})
assert.equal(items[0].displayName, 'quarter staff')
})
// The supported state for a shard whose operator never converted a cliloc file:
// no name, not a fabricated one. Clients render the item id, exactly as they did
// before the table existed.
test('shapeItems leaves displayName null for an unknown cliloc', async () => {
const items = await market.shapeItems({
items: [{ serial: '0x1', itemId: 3922, price: 100, name: null, cliloc: 9999999 }],
})
assert.equal(items[0].displayName, null)
})
// Unpriced rows are inventory, not listings. The shard drops them too; enforcing
// it here as well means a plugin that stops doing so cannot put un-buyable rows
// on the market page.
test('shapeItems drops unpriced listings', async () => {
const items = await market.shapeItems({
items: [
{ serial: '0x1', itemId: 1, price: 0 },
{ serial: '0x2', itemId: 2, price: -1 },
{ serial: '0x3', itemId: 3, price: 5 },
],
})
assert.deepEqual(items.map((i) => i.serial), ['0x3'])
})
test('shapeItems caps a pathological frame', async () => {
const many = Array.from({ length: market.MAX_ITEMS_PER_VENDOR + 50 }, (_, i) => ({
serial: `0x${i}`,
itemId: 1,
price: 1,
}))
const items = await market.shapeItems({ items: many })
assert.equal(items.length, market.MAX_ITEMS_PER_VENDOR)
})
test('shapeItems tolerates a frame with no items array', async () => {
assert.deepEqual(await market.shapeItems({}), [])
})
// ── Visibility projection ──────────────────────────────────────────────────
//
// The regression that matters. Part A pre-wired `market.ownerName` and
// `market.location` before the frame existed, and the sibling rule it pre-wired
// for leaderboards (`characterName`) turned out to be INERT because projectValue
// matches literal JSON keys. These assert the market rules actually bite — on the
// read model AND on the wire frame, which is why both carry the same key names.
const config = visibility.compileDefaults()
const listing = market.shapeListing({
serial: '0x40012ABC',
item_id: 3922,
hue: 0,
amount: 1,
price: 25000,
name: null,
cliloc: 1023721,
display_name: 'quarter staff',
child: 0,
vendor_serial: '0x40001234',
shop_name: "Darrow's Bargains",
owner_serial: '0x1A2B',
owner_name: 'Darrow',
map: 'Trammel',
x: 1421,
y: 1699,
z: 0,
region: 'Britain',
house: "Darrow's Villa",
updated_at: new Date(0),
})
test('market defaults expose owner and location (they are already public in game)', () => {
const out = visibility.projectFeature('market', listing, 'anonymous', config)
assert.equal(out.vendor.ownerName, 'Darrow')
assert.equal(out.vendor.location.region, 'Britain')
})
test('tightening market.ownerName hides it from below that rung', () => {
const tightened = { ...config, market: { ...config.market, fields: { ...config.market.fields, ownerName: 'staff' } } }
const anon = visibility.projectFeature('market', listing, 'anonymous', tightened)
const staff = visibility.projectFeature('market', listing, 'staff', tightened)
assert.equal('ownerName' in anon.vendor, false)
assert.equal(staff.vendor.ownerName, 'Darrow')
// The shop name is a separate field and must survive — hiding the owner is not
// the same as hiding the shop.
assert.equal(anon.vendor.shopName, "Darrow's Bargains")
})
// The whole reason `location` is one nested object: a single rule has to take the
// facet, the coordinates, the region and the house together. Five flat keys would
// be five rules that drift apart.
test('tightening market.location hides the whole location object at once', () => {
const tightened = { ...config, market: { ...config.market, fields: { ...config.market.fields, location: 'player' } } }
const anon = visibility.projectFeature('market', listing, 'anonymous', tightened)
const player = visibility.projectFeature('market', listing, 'player', tightened)
assert.equal('location' in anon.vendor, false)
assert.equal(player.vendor.location.map, 'Trammel')
})
// The same rules must bite on the LIVE frame, not just the stored read model —
// the market's SSE stream is off by default but an admin can turn it on, and a
// field rule that only worked on one of the two paths is exactly the leak §3.6.1
// records.
test('the same rules apply to the raw vendor.listing frame', () => {
const tightened = { ...config, market: { ...config.market, fields: { ...config.market.fields, ownerName: 'admin', location: 'admin' } } }
const out = visibility.projectFeature('market', FRAME, 'anonymous', tightened)
assert.equal('ownerName' in out, false)
assert.equal('location' in out, false)
assert.equal(out.shopName, "Darrow's Bargains")
})
// Rule 1 is not configurable and does not depend on the market rules at all: a
// frame that somehow carried an account name must never publish it.
test('acct and webId are stripped from a market payload regardless of config', () => {
const out = visibility.projectFeature(
'market',
{ serial: '0x1', ownerAcct: 'darrow', webId: '42', shopName: 'Shop' },
'staff',
config,
)
assert.equal('ownerAcct' in out, false)
assert.equal('webId' in out, false)
assert.equal(out.shopName, 'Shop')
})
// Both kinds must be attributed to a feature, or rule 2 makes them admin-only by
// omission — which would be a silent failure rather than a loud one.
test('both market kinds are mapped to the market feature', () => {
assert.equal(visibility.KIND_FEATURE.get('vendor.listing'), 'market')
assert.equal(visibility.KIND_FEATURE.get('vendor.listing.remove'), 'market')
})
// The market's live firehose is off by default (a page of whole vendor
// inventories is the site's biggest bandwidth item and no page needs it live),
// but the REST reads are unaffected — which is what `visibleKinds` ignoring the
// stream flag encodes.
test('market kinds are stream-suppressed by default but still readable', () => {
assert.equal(visibility.DEFAULT_STREAM_OFF.has('market'), true)
assert.equal(visibility.kindVisibleTo('vendor.listing', 'anonymous', config), false)
assert.equal(visibility.PUBLIC_KINDS.has('vendor.listing'), false)
assert.ok(visibility.visibleKinds('anonymous', config).includes('vendor.listing'))
})
test('an admin who enables the stream gets the frames', () => {
const on = { ...config, market: { ...config.market, stream: true } }
assert.equal(visibility.kindVisibleTo('vendor.listing', 'anonymous', on), true)
})
// Guards the stub above against silently doing nothing: if the model stopped
// going through db.lookup, every shapeItems assertion would still "pass" by
// resolving nothing, which is also what a real miss looks like.
test('the cliloc resolver is the path shapeItems resolves through', async () => {
const found = await clilocs.resolveMany([1023721])
assert.equal(found.get(1023721), 'quarter staff')
})