Two findings of the step-2 player walk (2026-09-27, both rigs). F6, option (b) of the org lead (D186): a code no recent issuer holds - every made-up one - was still asked of every other enabled server, and while any of them was down the redeem waited out its whole timeout (12 s on both rigs). The second pass now skips the servers the board poll last saw without a connected game; they count as offline without the wait. Issuers are still asked whatever their state, so a good code on a down server stays "unsure". Live on the walk core: 338 ms with five servers down, 360 ms with a rig stopped as well. F7 (D187): on Carbon a due audit sync went out the moment the sidecar reconnected, 80 s before "Server startup complete". The worldReady hold reads the stored hello, which is the OLD boot's until the poll reads the new one. reasonToSync now also holds while the stored state says the game is not connected (online 0), which the poll writes the moment the server goes away. titleSync already held on it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
432 lines
17 KiB
JavaScript
432 lines
17 KiB
JavaScript
// ── Identity: the fleet loop and the refusal ──────────────────────────────
|
|
//
|
|
// Two things in this file are worth more than the rest, and both are about
|
|
// telling answers apart that a naive implementation collapses:
|
|
//
|
|
// • **A code is minted by ONE server** and the player types six characters into
|
|
// a browser. The servers that minted a code recently are asked first, the
|
|
// rest only when none of them had it, each group in parallel (D24, F5, F6) —
|
|
// and "every reachable server said no" is NOT the same answer as "a server
|
|
// that minted a code could not be reached". The second is the case where the
|
|
// player's code is perfectly good and the advice "run /link again" is useless,
|
|
// because it sends them back to the server that is down.
|
|
//
|
|
// • **A Steam id another account holds is refused, never moved** (D23). Once
|
|
// phase 7 grants permissions against a link and phase 13 hangs entitlements
|
|
// off it, a silent move is an account takeover performed by typing six
|
|
// characters.
|
|
|
|
const test = require('node:test')
|
|
const assert = require('node:assert')
|
|
|
|
const { fakeCtx } = require('./_fakes')
|
|
|
|
/**
|
|
* Installs a ctx whose `db.query` answers from a small script.
|
|
*
|
|
* `rows` is consulted by the first word of the statement, which is as much SQL as
|
|
* these tests should know: the point of each one is the decision the model makes,
|
|
* not the shape of a SELECT it delegates.
|
|
*/
|
|
function withCore({ select = [], onInsert = null } = {}) {
|
|
const queries = []
|
|
|
|
const ctx = fakeCtx({
|
|
db: {
|
|
query: (sql, params) => {
|
|
queries.push({ sql, params })
|
|
|
|
const verb = sql.trim().split(/\s+/)[0].toUpperCase()
|
|
|
|
if (verb === 'SELECT') {
|
|
const next = Array.isArray(select) ? select.shift() : select
|
|
return Promise.resolve(next || [])
|
|
}
|
|
|
|
if (verb === 'INSERT' && onInsert) return onInsert(params)
|
|
|
|
return Promise.resolve({ affectedRows: 1 })
|
|
},
|
|
pool: {},
|
|
},
|
|
})
|
|
|
|
require('../core')._reset()
|
|
require('../core').init(ctx)
|
|
|
|
return { ctx, queries }
|
|
}
|
|
|
|
/**
|
|
* A fleet of `n` servers, and a sidecar that answers from a script.
|
|
*
|
|
* `issuers` are the servers the site saw mint a link code in the last few
|
|
* minutes (`account.link.requested`, F5) — none by default, which is also what a
|
|
* code typed before its frame was ingested looks like.
|
|
*
|
|
* `down` are the servers the board poll last saw without a connected game
|
|
* (`online: 0`); every other server has no state row, which is asked like an up one.
|
|
*/
|
|
function fleetOf(replies, issuers = [], down = []) {
|
|
const servers = require('../model/servers/servers.model')
|
|
const serversDb = require('../model/servers/servers.db')
|
|
const sidecar = require('../sidecarClient')
|
|
const linksDb = require('../model/links/links.db')
|
|
|
|
const asked = []
|
|
const ids = Object.keys(replies)
|
|
|
|
servers.listForPolling = async () => ids.map((id) => ({ id, baseUrl: `http://${id}`, token: 't' }))
|
|
serversDb.listState = async () => down.map((serverId) => ({ serverId, online: 0 }))
|
|
linksDb.recentLinkIssuers = async () => [...issuers]
|
|
|
|
sidecar.confirmLink = async (server, code) => {
|
|
asked.push({ server: server.id, code })
|
|
return replies[server.id]
|
|
}
|
|
|
|
return asked
|
|
}
|
|
|
|
/** The two replies a reachable sidecar can carry, and the one it cannot. */
|
|
const linkOk = (steamId, name) => ({ ok: true, status: 'ok', data: { kind: 'link.ok', steamId, name } })
|
|
const linkRefused = { ok: true, status: 'ok', data: { kind: 'link.error', reason: 'unknown' } }
|
|
const unreachable = { ok: false, status: 'transport-error', data: null }
|
|
|
|
test('every server is asked until one recognises the code, and the one that answered is recorded', async () => {
|
|
const { queries } = withCore({ select: [[], [{ steamId: '7656', userId: 4, name: 'Wanderer', serverId: 'b' }]] })
|
|
const links = require('../model/links/links.model')
|
|
|
|
const asked = fleetOf({ a: linkRefused, b: linkOk('7656', 'Wanderer') })
|
|
|
|
const result = await links.redeem({ code: 'K7M2PQ', userId: 4 })
|
|
|
|
assert.equal(result.ok, true)
|
|
assert.equal(result.link.steamId, '7656')
|
|
|
|
// Both servers were asked, in order, with the same code — and the loop stopped
|
|
// at the one that said yes.
|
|
assert.deepEqual(asked, [{ server: 'a', code: 'K7M2PQ' }, { server: 'b', code: 'K7M2PQ' }])
|
|
|
|
// The server that minted it is stored. It is not part of the identity — a link
|
|
// is fleet-wide — but it is where a support conversation starts.
|
|
const insert = queries.find((q) => q.sql.trim().toUpperCase().startsWith('INSERT'))
|
|
assert.deepEqual(insert.params, ['7656', 4, 'Wanderer', 'b'])
|
|
})
|
|
|
|
test('when a server that minted a code answers it, the rest are never asked', async () => {
|
|
withCore({ select: [[], [{ steamId: '7656', userId: 4 }]] })
|
|
const links = require('../model/links/links.model')
|
|
|
|
const asked = fleetOf({ a: linkOk('7656', 'Wanderer'), b: linkRefused, c: linkRefused }, ['a'])
|
|
|
|
await links.redeem({ code: 'K7M2PQ', userId: 4 })
|
|
|
|
// A code is spent on the plugin's FIRST lookup, so carrying on after a yes
|
|
// would be asking other game hosts to look up a secret that has already been
|
|
// redeemed.
|
|
assert.deepEqual(asked.map((a) => a.server), ['a'])
|
|
})
|
|
|
|
test('a code typed before its mint was ingested is still found, on a server nobody expected', async () => {
|
|
// Ingest polls every few seconds; a fast typist can beat it. With no recent
|
|
// mint on record the whole fleet is asked, and the code is where it is.
|
|
withCore({ select: [[], [{ steamId: '7656', userId: 4, name: 'Wanderer', serverId: 'c' }]] })
|
|
const links = require('../model/links/links.model')
|
|
|
|
fleetOf({ a: linkRefused, b: unreachable, c: linkOk('7656', 'Wanderer') })
|
|
|
|
const result = await links.redeem({ code: 'K7M2PQ', userId: 4 })
|
|
assert.equal(result.ok, true)
|
|
})
|
|
|
|
test('a Steam id another account holds is refused, not moved — and the loop stops', async () => {
|
|
// The whole of D23 in one assertion. The holder is named because the player is
|
|
// signed in and the advice ("sign in as that account, or run /unlink") is
|
|
// unusable without it.
|
|
withCore({ select: [[{ steamId: '7656', userId: 9, username: 'someone-else' }]] })
|
|
const links = require('../model/links/links.model')
|
|
|
|
const asked = fleetOf({ a: linkOk('7656', 'Wanderer'), b: linkRefused }, ['a'])
|
|
|
|
const result = await links.redeem({ code: 'K7M2PQ', userId: 4 })
|
|
|
|
assert.equal(result.ok, false)
|
|
assert.equal(result.reason, 'taken')
|
|
assert.equal(result.username, 'someone-else')
|
|
|
|
// Asking the rest of the fleet would answer the same question more slowly: the
|
|
// verdict is about the Steam id, not about this server.
|
|
assert.deepEqual(asked.map((a) => a.server), ['a'])
|
|
})
|
|
|
|
test('a code already redeemed by the SAME user is a success, not an error', async () => {
|
|
withCore({ select: [[{ steamId: '7656', userId: 4, name: 'Wanderer', serverId: 'a' }]] })
|
|
const links = require('../model/links/links.model')
|
|
|
|
fleetOf({ a: linkOk('7656', 'Wanderer') })
|
|
|
|
const result = await links.redeem({ code: 'K7M2PQ', userId: 4 })
|
|
|
|
// A player who pressed the button twice, or whose confirmation was applied on a
|
|
// request that then timed out. Reporting that as a failure would send them to
|
|
// run `/link` again for a link they already have.
|
|
assert.equal(result.ok, true)
|
|
assert.equal(result.already, true)
|
|
})
|
|
|
|
test('"every reachable server refused" is not the same answer as "a server that minted a code was unreachable"', async () => {
|
|
withCore()
|
|
const links = require('../model/links/links.model')
|
|
|
|
fleetOf({ a: linkRefused, b: unreachable }, ['b'])
|
|
|
|
const result = await links.redeem({ code: 'K7M2PQ', userId: 4 })
|
|
|
|
// The failure this prevents: a player linked on the server that is down, is
|
|
// told their code is wrong, runs `/link` again on that same server, and is told
|
|
// the same thing for as long as it stays down.
|
|
assert.equal(result.reason, 'unsure')
|
|
})
|
|
|
|
test('a fleet nobody can reach is offline, and a fleet that all refused is a bad code', async () => {
|
|
withCore()
|
|
let links = require('../model/links/links.model')
|
|
|
|
fleetOf({ a: unreachable, b: unreachable })
|
|
assert.equal((await links.redeem({ code: 'K7M2PQ', userId: 4 })).reason, 'offline')
|
|
|
|
withCore()
|
|
links = require('../model/links/links.model')
|
|
|
|
fleetOf({ a: linkRefused, b: linkRefused })
|
|
assert.equal((await links.redeem({ code: 'K7M2PQ', userId: 4 })).reason, 'rejected')
|
|
})
|
|
|
|
test('a site with no servers configured says so rather than that the code is wrong', async () => {
|
|
withCore()
|
|
const links = require('../model/links/links.model')
|
|
|
|
fleetOf({})
|
|
|
|
assert.equal((await links.redeem({ code: 'K7M2PQ', userId: 4 })).reason, 'no-servers')
|
|
})
|
|
|
|
test('two confirmations of one Steam id race into the primary key, not into a 500', async () => {
|
|
// The window the PRIMARY KEY exists for: both requests read "not linked", both
|
|
// write. The second insert is refused by the key, and the refusal has to become
|
|
// the same sentence the check above produces — otherwise one of two players
|
|
// pressing a button at the same moment gets an internal error.
|
|
const dup = Object.assign(new Error('duplicate'), { code: 'ER_DUP_ENTRY' })
|
|
|
|
withCore({
|
|
select: [[], [{ steamId: '7656', userId: 9, username: 'someone-else' }]],
|
|
onInsert: () => Promise.reject(dup),
|
|
})
|
|
const links = require('../model/links/links.model')
|
|
|
|
fleetOf({ a: linkOk('7656', 'Wanderer') })
|
|
|
|
const result = await links.redeem({ code: 'K7M2PQ', userId: 4 })
|
|
|
|
assert.equal(result.ok, false)
|
|
assert.equal(result.reason, 'taken')
|
|
assert.equal(result.username, 'someone-else')
|
|
})
|
|
|
|
test('the same race, won by the caller, is a success', async () => {
|
|
const dup = Object.assign(new Error('duplicate'), { errno: 1062 })
|
|
|
|
withCore({
|
|
select: [[], [{ steamId: '7656', userId: 4, name: 'Wanderer', serverId: 'a' }]],
|
|
onInsert: () => Promise.reject(dup),
|
|
})
|
|
const links = require('../model/links/links.model')
|
|
|
|
fleetOf({ a: linkOk('7656', 'Wanderer') })
|
|
|
|
const result = await links.redeem({ code: 'K7M2PQ', userId: 4 })
|
|
|
|
assert.equal(result.ok, true)
|
|
assert.equal(result.already, true)
|
|
})
|
|
|
|
test('a link is never shaped with anything a code could be recovered from', async () => {
|
|
withCore()
|
|
const links = require('../model/links/links.model')
|
|
|
|
const shaped = links.shape({
|
|
steamId: '7656',
|
|
userId: 4,
|
|
username: 'someone',
|
|
name: 'Wanderer',
|
|
serverId: 'a',
|
|
linkedAt: '2026-09-21T00:00:00Z',
|
|
})
|
|
|
|
// `userId` and `username` are deliberately absent: the caller is the user, and
|
|
// a list that carried somebody's website username would be a different fact
|
|
// from "you hold this Steam id".
|
|
assert.deepEqual(Object.keys(shaped).sort(), ['linkedAt', 'name', 'serverId', 'steamId'])
|
|
})
|
|
|
|
test('an unlink is scoped by user in the statement, not checked before it', async () => {
|
|
const { queries } = withCore()
|
|
const links = require('../model/links/links.model')
|
|
|
|
await links.unlinkOwned('7656', 4)
|
|
|
|
const del = queries.find((q) => q.sql.trim().toUpperCase().startsWith('DELETE'))
|
|
|
|
// Read-then-write would leave a gap between the ownership test and the
|
|
// deletion; one statement closes it, and the row count is what tells "removed"
|
|
// from "was not yours".
|
|
assert.ok(del.sql.includes('user_id = ?'))
|
|
assert.deepEqual(del.params, ['7656', 4])
|
|
})
|
|
|
|
test('the in-game unlink is scoped by Steam id alone, because that is the authority', async () => {
|
|
const { queries } = withCore()
|
|
const links = require('../model/links/links.model')
|
|
|
|
await links.unlinkFromGame('7656')
|
|
|
|
const del = queries.find((q) => q.sql.trim().toUpperCase().startsWith('DELETE'))
|
|
|
|
// Whoever is connected to the game as that Steam account is who it is — a
|
|
// stronger proof of ownership than the site can obtain any other way. Scoping
|
|
// this by website user would make `/unlink` fail for the one player who needs
|
|
// it: the one who linked the wrong account.
|
|
assert.ok(!del.sql.includes('user_id'))
|
|
assert.deepEqual(del.params, ['7656'])
|
|
})
|
|
|
|
test('a player sees the name the GAME last saw, not the one they linked under', async () => {
|
|
withCore({ select: [[{ steamId: '7656', userId: 4, name: 'Wanderer-old', playerName: 'Wanderer', serverId: 'a', linkedAt: 'x' }]] })
|
|
const links = require('../model/links/links.model')
|
|
|
|
const [link] = await links.listForUser(4)
|
|
|
|
// Found in a browser: staff saw `Wanderer` on the admin panel and the player
|
|
// saw `Wanderer-old` on their own page — the same person, labelled two ways on
|
|
// one site, because a Rust name changes on a whim and only one of the two reads
|
|
// was joining `rust_players`.
|
|
assert.equal(link.name, 'Wanderer')
|
|
|
|
// And the fallback still holds for a link whose account has never played.
|
|
withCore({ select: [[{ steamId: '7656', userId: 4, name: 'Wanderer-old', playerName: null, linkedAt: 'x' }]] })
|
|
const again = require('../model/links/links.model')
|
|
assert.equal((await again.listForUser(4))[0].name, 'Wanderer-old')
|
|
})
|
|
|
|
test('a new link and a removed one ask core to reconcile Teams (D57)', async () => {
|
|
// A clan member's website account comes from this table. Without the request,
|
|
// somebody who links today is not in their clan's Team until core's next
|
|
// scheduled sweep.
|
|
const { ctx } = withCore({ select: [[], [{ steamId: '7656', userId: 4 }]] })
|
|
const links = require('../model/links/links.model')
|
|
fleetOf({ a: linkOk('7656', 'Wanderer') })
|
|
|
|
await links.redeem({ code: 'K7M2PQ', userId: 4 })
|
|
assert.equal(ctx.teams.reconcile.calls.length, 1)
|
|
|
|
await links.unlinkAnyOwner('7656')
|
|
assert.equal(ctx.teams.reconcile.calls.length, 2)
|
|
})
|
|
|
|
test('a link that was already there asks for nothing', async () => {
|
|
const { ctx } = withCore({ select: [[{ steamId: '7656', userId: 4 }]] })
|
|
const links = require('../model/links/links.model')
|
|
fleetOf({ a: linkOk('7656', 'Wanderer') })
|
|
|
|
const result = await links.redeem({ code: 'K7M2PQ', userId: 4 })
|
|
assert.equal(result.already, true)
|
|
assert.equal(ctx.teams.reconcile.calls.length, 0)
|
|
})
|
|
|
|
test('a dead server that minted nothing does not make a wrong code "still good" (F5)', async () => {
|
|
// The first walk: five of seven servers down, and a made-up ZZZZZZ was answered
|
|
// "one of the servers could not be reached — your code is still good". A server
|
|
// that has not minted a code in the window cannot hold this one.
|
|
withCore()
|
|
const links = require('../model/links/links.model')
|
|
|
|
fleetOf({ a: linkRefused, b: unreachable, c: unreachable, d: unreachable }, ['a'])
|
|
|
|
assert.equal((await links.redeem({ code: 'ZZZZZZ', userId: 4 })).reason, 'rejected')
|
|
})
|
|
|
|
test('the fleet is asked in parallel, not one dead server after another (F6)', async () => {
|
|
// One at a time, the walk waited about four seconds on each dead server in turn —
|
|
// twenty-one seconds, a successful link included.
|
|
withCore({ select: [[], [{ steamId: '7656', userId: 4, name: 'Wanderer', serverId: 'e' }]] })
|
|
const links = require('../model/links/links.model')
|
|
const sidecar = require('../sidecarClient')
|
|
|
|
fleetOf({ a: unreachable, b: unreachable, c: unreachable, d: unreachable, e: linkOk('7656', 'Wanderer') })
|
|
|
|
let inFlight = 0
|
|
let most = 0
|
|
const scripted = sidecar.confirmLink
|
|
sidecar.confirmLink = async (server, code) => {
|
|
inFlight += 1
|
|
most = Math.max(most, inFlight)
|
|
await new Promise((resolve) => setTimeout(resolve, 20))
|
|
inFlight -= 1
|
|
return scripted(server, code)
|
|
}
|
|
|
|
const result = await links.redeem({ code: 'K7M2PQ', userId: 4 })
|
|
assert.equal(result.ok, true)
|
|
assert.equal(most, 5, 'every server was being asked at once')
|
|
})
|
|
|
|
test('a server the poll saw go down is not waited on for a code nobody minted (F6, option b)', async () => {
|
|
// The step-2 walk: with one rig stopped, a made-up code took twelve seconds on
|
|
// both frameworks — in parallel, but still one whole timeout on the dead server.
|
|
withCore()
|
|
const links = require('../model/links/links.model')
|
|
|
|
const asked = fleetOf({ a: linkRefused, b: unreachable, c: linkRefused }, [], ['b'])
|
|
|
|
assert.equal((await links.redeem({ code: 'ZZZZZZ', userId: 4 })).reason, 'rejected')
|
|
assert.deepEqual(asked.map((x) => x.server).sort(), ['a', 'c'], 'the server known down was not asked')
|
|
})
|
|
|
|
test('an issuer is asked even when the poll saw it go down, so a good code stays "unsure"', async () => {
|
|
// Skipping is for the servers that cannot hold the code. A server that minted
|
|
// one in the window might — and a player whose code is on it must not be told
|
|
// the code is wrong.
|
|
withCore()
|
|
const links = require('../model/links/links.model')
|
|
|
|
const asked = fleetOf({ a: unreachable, b: linkRefused }, ['a'], ['a'])
|
|
|
|
assert.equal((await links.redeem({ code: 'K7M2PQ', userId: 4 })).reason, 'unsure')
|
|
assert.ok(asked.some((x) => x.server === 'a'), 'the down issuer was still asked')
|
|
})
|
|
|
|
test('a fleet the poll saw all go down is offline, without asking any of it', async () => {
|
|
withCore()
|
|
const links = require('../model/links/links.model')
|
|
|
|
const asked = fleetOf({ a: unreachable, b: unreachable }, [], ['a', 'b'])
|
|
|
|
assert.equal((await links.redeem({ code: 'ZZZZZZ', userId: 4 })).reason, 'offline')
|
|
assert.equal(asked.length, 0)
|
|
})
|
|
|
|
test('a state read that fails asks everybody, which is slow and never wrong', async () => {
|
|
withCore()
|
|
const links = require('../model/links/links.model')
|
|
const serversDb = require('../model/servers/servers.db')
|
|
|
|
const asked = fleetOf({ a: linkRefused, b: linkRefused })
|
|
serversDb.listState = async () => {
|
|
throw new Error('pool closed')
|
|
}
|
|
|
|
assert.equal((await links.redeem({ code: 'ZZZZZZ', userId: 4 })).reason, 'rejected')
|
|
assert.equal(asked.length, 2)
|
|
})
|