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>
115 lines
4.7 KiB
JavaScript
115 lines
4.7 KiB
JavaScript
const { test, beforeEach } = require('node:test')
|
|
const assert = require('node:assert/strict')
|
|
|
|
const shardIngest = require('../utils/shardIngest')
|
|
|
|
// Protocol 3.0 vendor.listing / vendor.listing.remove routing. Same shape as
|
|
// shardIngest.points.test.js: stubbed deps, asserting where the dispatcher sends
|
|
// the frame and whether it is appended to the event log.
|
|
function makeDeps() {
|
|
const calls = { upserts: [], removes: [], appended: [], broadcast: [] }
|
|
const noop = async () => {}
|
|
return {
|
|
calls,
|
|
shardEvents: { append: async (row) => { calls.appended.push(row); return true } },
|
|
shardState: {
|
|
// Present so any stray routing is a harmless no-op rather than a crash.
|
|
clearOnline: noop, upsertOnline: noop, setOffline: noop, upsertHouse: noop,
|
|
addEconomySample: noop, setRuleset: noop, upsertPointsBoard: noop,
|
|
},
|
|
shardMarket: {
|
|
upsertVendor: async (ev) => { calls.upserts.push(ev) },
|
|
removeVendor: async (serial) => { calls.removes.push(serial) },
|
|
},
|
|
shardLinks: { removeByAccount: noop },
|
|
uoLinkConfig: { recordStatus: noop },
|
|
broadcast: (ev) => { calls.broadcast.push(ev) },
|
|
pushDispatch: async () => {},
|
|
log: { warn() {}, info() {}, error() {} },
|
|
}
|
|
}
|
|
|
|
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: [
|
|
{ serial: '0x40012ABC', itemId: 3922, hue: 0, amount: 1, price: 25000, name: null, cliloc: 1023721 },
|
|
{ serial: '0x40012ABD', itemId: 7026, hue: 1157, amount: 3, price: 500, name: 'a shard sigil', cliloc: 1041243 },
|
|
],
|
|
}
|
|
|
|
beforeEach(() => shardIngest.reset())
|
|
|
|
test('vendor.listing routes to the market model with the whole frame', async () => {
|
|
const deps = makeDeps()
|
|
await shardIngest.ingest(FRAME, deps)
|
|
assert.equal(deps.calls.upserts.length, 1)
|
|
const stored = deps.calls.upserts[0]
|
|
assert.equal(stored.serial, '0x40001234')
|
|
assert.equal(stored.location.region, 'Britain')
|
|
assert.equal(stored.items.length, 2)
|
|
})
|
|
|
|
test('vendor.listing.remove routes to removeVendor with the serial', async () => {
|
|
const deps = makeDeps()
|
|
await shardIngest.ingest({ kind: 'vendor.listing.remove', t: 2000, serial: '0x40001234' }, deps)
|
|
assert.deepEqual(deps.calls.removes, ['0x40001234'])
|
|
assert.equal(deps.calls.upserts.length, 0)
|
|
})
|
|
|
|
// The market IS the state. One frame carries up to 250 listings and the sweep
|
|
// re-emits a shop on any price change, so logging would turn shard_events into a
|
|
// price history nobody reads — the strongest case of the three v3 kinds.
|
|
test('neither market kind is appended to the event log', async () => {
|
|
const deps = makeDeps()
|
|
const a = await shardIngest.ingest(FRAME, deps)
|
|
const b = await shardIngest.ingest({ kind: 'vendor.listing.remove', serial: '0x40001234' }, deps)
|
|
assert.equal(a.logged, false)
|
|
assert.equal(b.logged, false)
|
|
assert.equal(deps.calls.appended.length, 0)
|
|
assert.equal(shardIngest.LOGGED_KINDS.has('vendor.listing'), false)
|
|
assert.equal(shardIngest.LOGGED_KINDS.has('vendor.listing.remove'), false)
|
|
})
|
|
|
|
// Broadcast is unconditional at this layer — whether it actually reaches anyone
|
|
// is shardBroadcast's call, and the market feature ships with its stream off.
|
|
test('vendor.listing is handed to the broadcaster', async () => {
|
|
const deps = makeDeps()
|
|
await shardIngest.ingest(FRAME, deps)
|
|
assert.equal(deps.calls.broadcast.length, 1)
|
|
assert.equal(deps.calls.broadcast[0].kind, 'vendor.listing')
|
|
})
|
|
|
|
test('a backfilled vendor.listing still stores but does not broadcast', async () => {
|
|
const deps = makeDeps()
|
|
await shardIngest.ingest(FRAME, { ...deps, fromBackfill: true })
|
|
assert.equal(deps.calls.upserts.length, 1)
|
|
assert.equal(deps.calls.broadcast.length, 0)
|
|
})
|
|
|
|
// The reconnect backfill replays the whole index through this path, so a single
|
|
// bad vendor must not abort it.
|
|
test('an upsertVendor failure does not throw or stop the broadcast', async () => {
|
|
const deps = makeDeps()
|
|
deps.shardMarket.upsertVendor = 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)
|
|
})
|
|
|
|
// Each vendor is its own row; the frame is authoritative for that vendor only.
|
|
test('two vendors are stored independently', async () => {
|
|
const deps = makeDeps()
|
|
await shardIngest.ingest(FRAME, deps)
|
|
await shardIngest.ingest({ ...FRAME, serial: '0x40009999', shopName: 'Second Shop' }, deps)
|
|
assert.deepEqual(deps.calls.upserts.map((v) => v.serial), ['0x40001234', '0x40009999'])
|
|
})
|