mirror of
https://github.com/Such-Software/wownerogue.git
synced 2026-10-21 08:08:32 +02:00
chore: prepare the repository for public release
Removes operator-private material from the shipped tree, corrects documentation
that had drifted from the code, deletes dead code, and applies a consistent
prose style.
Private material moved out of the public tree (docs/private/, git-ignored):
- the wallet keyfile recovery runbook and its helper scripts, which together
published the mainnet wallet file path, the service account, backup snapshot
filenames, and the wallet RPC listener state. The helper duplicated the same
constants, so relocating the document alone would not have removed them.
- the stagenet parallel-candidate runbook and its fixtures.
- the dated production-state record, the internal remediation plan, and the
TODO list. None are reference documentation for an outside reader.
Redacted or genericised:
- operator home-directory paths in shipped scripts and units. The asset build
script now takes KENNEY_ASSETS_DIR instead of a hard-coded path.
- private LAN addresses in tests, env examples and deploy docs, replaced with
RFC 5737 documentation addresses.
- the default alert sender and the notification hub example.
- the Monero wallet unit now matches the Wownero one: a dedicated service
account and a config file, instead of the operator's login account with the
wallet password in argv where any local user can read it from /proc.
Ignore rules now cover every .env variant rather than the bare name, plus local
agent state. The deployment docs instruct operators to create .env.mainnet and
.env.stagenet, which the exact-name rule would have allowed to be committed.
Documentation corrected against the code: per-chain dungeon level counts, the
premium unlock rule, Tavern chat scope, backup retention, the chain-profile
fallback, the environment template location, and several sections describing
shipped work as pending.
Dead code removed: configPersistence, rpccalls, a duplicate vendored ROT.js
bundle, the unused tile-picker overlay, the unreachable legacy game-size path
and its vestigial parameter, spectator stubs that shadowed the real
SpectatorManager, and commented-out debugging. nip98 now imports the shared
nostr loader rather than duplicating it.
Prose style: em-dashes removed throughout in favour of colons, semicolons and
commas; comments restated as present-tense descriptions of the code rather than
narration of past changes.
127 suites / 1015 tests pass.
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
* Admin routes extraction smoke test (Phase 4.1).
|
||||
*
|
||||
* Verifies the extracted admin route factory loads and registers all expected routes
|
||||
* with a stub ctx — catches load-time/reference errors from the index.js -> routes/admin.js
|
||||
* with a stub ctx: catches load-time/reference errors from the index.js -> routes/admin.js
|
||||
* split without needing a live server.
|
||||
*/
|
||||
|
||||
|
||||
@@ -217,7 +217,7 @@ describe('Batch payout dispatch', () => {
|
||||
test('a PRE-BROADCAST failure returns the batch to pending, not needs_review', async () => {
|
||||
// Address validation and the transfer gate run strictly before transfer_split can send
|
||||
// anything, so those rows are provably unsent and safe to retry. Quarantining them in
|
||||
// needs_review parked them permanently — the retry service deliberately skips that status.
|
||||
// needs_review parked them permanently: the retry service deliberately skips that status.
|
||||
const preBroadcast = Object.assign(new Error('One or more payout addresses failed validation.'),
|
||||
{ preBroadcast: true });
|
||||
const walletService = {
|
||||
@@ -238,7 +238,7 @@ describe('Batch payout dispatch', () => {
|
||||
});
|
||||
|
||||
test('an ambiguous POST-broadcast failure still quarantines the batch', async () => {
|
||||
const ambiguous = new Error('socket hang up'); // no preBroadcast tag — may have sent
|
||||
const ambiguous = new Error('socket hang up'); // no preBroadcast tag: may have sent
|
||||
const walletService = {
|
||||
getBalance: jest.fn().mockResolvedValue({ balance: '10000000000000', unlocked_balance: '10000000000000' }),
|
||||
processPayout: jest.fn(),
|
||||
|
||||
@@ -10,7 +10,7 @@ const Entitlements = require('../src/multiplayer/entitlements');
|
||||
const CatalogService = require('../src/services/catalogService');
|
||||
const { snapshotForUser, DEFAULT_CATALOG } = Entitlements;
|
||||
|
||||
describe('three-way unlock — default (seed) catalog', () => {
|
||||
describe('three-way unlock: default (seed) catalog', () => {
|
||||
test('free pack is always unlocked; premium packs are locked with no spend', () => {
|
||||
const s = snapshotForUser({ credits: 0, total_credits_purchased: 0 });
|
||||
expect(s.packs['original']).toBe(true); // free baseline
|
||||
@@ -55,7 +55,7 @@ describe('three-way unlock — default (seed) catalog', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('THE bug fix — differentiated thresholds', () => {
|
||||
describe('THE bug fix: differentiated thresholds', () => {
|
||||
const catalog = {
|
||||
'skin-a': { id: 'skin-a', label: 'A', tier: 0, unlockMinCredits: 1, grantOnly: false },
|
||||
'pack-b': { id: 'pack-b', label: 'B', tier: 0, unlockMinCredits: 10, grantOnly: false }
|
||||
@@ -92,7 +92,7 @@ describe('grant-only packs', () => {
|
||||
const catalog = {
|
||||
'exclusive': { id: 'exclusive', label: 'X', tier: 0, unlockMinCredits: null, grantOnly: true }
|
||||
};
|
||||
test('grant-only is not unlocked by spend or tier — only an explicit grant', () => {
|
||||
test('grant-only is not unlocked by spend or tier: only an explicit grant', () => {
|
||||
expect(snapshotForUser({ total_credits_purchased: 9999, premium_level: 'operator' }, [], catalog).packs['exclusive']).toBe(false);
|
||||
expect(snapshotForUser({}, [{ pack_id: 'exclusive' }], catalog).packs['exclusive']).toBe(true);
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* ChainProfile registry (Pillar 3 foundation) — one source for chain params across Smirk's five
|
||||
* ChainProfile registry (Pillar 3 foundation): one source for chain params across Smirk's five
|
||||
* PoW chains, and the fix for decimals silently defaulting to 12 for anything but WOW/XMR.
|
||||
*/
|
||||
const ChainProfile = require('../src/chain/chainProfile');
|
||||
|
||||
@@ -18,7 +18,7 @@ describe('SocketChatProvider', () => {
|
||||
const broadcast = [];
|
||||
const history = { saveMessage: (m) => { saved.push(m); return Promise.resolve(); } };
|
||||
const broadcastManager = {
|
||||
// The 4th arg is the sanitized public id (never the raw socket.id — S1 defense in depth).
|
||||
// The 4th arg is the sanitized public id (never the raw socket.id: S1 defense in depth).
|
||||
broadcastChatMessage: (username, text, ts, publicId) => broadcast.push({ username, text, ts, publicId })
|
||||
};
|
||||
const provider = new SocketChatProvider({ io, broadcastManager, historyManager: history });
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Credits unification, step ②: a direct/single_game entry is recorded as "buy 1 credit and
|
||||
* spend it" — the balance nets to zero but total_credits_purchased advances, so direct play
|
||||
* spend it": the balance nets to zero but total_credits_purchased advances, so direct play
|
||||
* unlocks the same tier/threshold cosmetics as buying credits.
|
||||
*/
|
||||
|
||||
@@ -40,7 +40,7 @@ describe('recordDirectEntryPurchase', () => {
|
||||
const rec = await gmm.recordDirectEntryPurchase('sock');
|
||||
expect(rec).toBeTruthy();
|
||||
expect(rec.totalCreditsPurchased).toBe(1);
|
||||
expect(rec.balance).toBe(0); // net zero — bought and spent
|
||||
expect(rec.balance).toBe(0); // net zero: bought and spent
|
||||
|
||||
const insert = client.query.mock.calls.find(c => /INSERT INTO credit_transactions/i.test(c[0]));
|
||||
expect(insert).toBeTruthy();
|
||||
|
||||
@@ -17,8 +17,8 @@ describe('cosmetic entitlements', () => {
|
||||
expect(entitlements.premium).toBe(true);
|
||||
expect(entitlements.level).toBe('free'); // spend unlocks packs by threshold, but isn't a premium tier
|
||||
expect(entitlements.packs['generated-skins']).toBe(true); // threshold 1
|
||||
expect(entitlements.packs['iso-dungeon']).toBe(true); // threshold 10 — just reached
|
||||
expect(entitlements.packs['kenney-3d-characters']).toBe(false); // threshold 25 — not yet
|
||||
expect(entitlements.packs['iso-dungeon']).toBe(true); // threshold 10: just reached
|
||||
expect(entitlements.packs['kenney-3d-characters']).toBe(false); // threshold 25: not yet
|
||||
expect(Entitlements.normalizeAppearance({ avatar: 'monero-knight' }, entitlements).avatar).toBe('monero-knight');
|
||||
});
|
||||
|
||||
|
||||
@@ -579,7 +579,7 @@ describe('EnvironmentValidator production launch gate', () => {
|
||||
'Production financial workers require WALLET_RPC_USER and WALLET_RPC_PASSWORD for wallet-rpc Digest authentication.'
|
||||
);
|
||||
|
||||
const remote = productionEnv({ PRIMARY_WALLET_ENDPOINT: 'http://10.42.1.20:38083' });
|
||||
const remote = productionEnv({ PRIMARY_WALLET_ENDPOINT: 'http://192.0.2.20:38083' });
|
||||
const remoteResult = new EnvironmentValidator({ env: remote, logger: silentLogger }).validate(config());
|
||||
expect(remoteResult.errors).toContain(
|
||||
'Production wallet-rpc over plaintext HTTP must bind to a loopback address.'
|
||||
@@ -603,7 +603,7 @@ describe('EnvironmentValidator production launch gate', () => {
|
||||
MONERO_NETWORK: 'mainnet',
|
||||
WALLET_RPC_USER: '',
|
||||
WALLET_RPC_PASSWORD: '',
|
||||
PRIMARY_WALLET_ENDPOINT: 'http://10.42.1.20:38083'
|
||||
PRIMARY_WALLET_ENDPOINT: 'http://192.0.2.20:38083'
|
||||
});
|
||||
|
||||
const errors = new EnvironmentValidator({ env, logger: silentLogger })
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
*
|
||||
* 1. No-address match winner reconciliation: setUserPayoutAddress converts a claimable
|
||||
* 'needs_review' match payout (reason 'match_winner_no_address') into a sendable 'pending'
|
||||
* payout once a real address is set — and NEVER touches other 'needs_review' rows (e.g. the
|
||||
* payout once a real address is set, and NEVER touches other 'needs_review' rows (e.g. the
|
||||
* ambiguous single-payout rows, which would double-pay if reset).
|
||||
* 2. Match boot-recovery age guard: _recoverAbandonedMatches reclaims all in-flight matches
|
||||
* immediately in single-instance mode, but only aged-out ones when MATCH_SINGLE_INSTANCE=false.
|
||||
@@ -43,14 +43,14 @@ function buildGmm() {
|
||||
return { gmm, db, calls };
|
||||
}
|
||||
|
||||
describe('Follow-up 1 — no-address match winner reconciliation', () => {
|
||||
describe('Follow-up 1: no-address match winner reconciliation', () => {
|
||||
test('converts claimable rows and kicks the batcher when an address is set', async () => {
|
||||
const { gmm, db } = buildGmm();
|
||||
const ok = await gmm.setUserPayoutAddress('sock', 'Wo3pRealAddress');
|
||||
expect(ok).toBe(true);
|
||||
const payoutUpdate = db.query.mock.calls.find(c => /UPDATE\s+payouts/i.test(c[0]));
|
||||
expect(payoutUpdate).toBeTruthy();
|
||||
// Narrow, safe filter — only explicit no-address liabilities, never generic needs_review.
|
||||
// Narrow, safe filter: only explicit no-address liabilities, never generic needs_review.
|
||||
expect(payoutUpdate[0]).toMatch(/reason\s+IN\s*\(\s*'match_winner_no_address'\s*,\s*'solo_winner_no_address'\s*\)/);
|
||||
expect(payoutUpdate[0]).toMatch(/status\s*=\s*'needs_review'/);
|
||||
expect(payoutUpdate[0]).toMatch(/PENDING_NO_ADDRESS/);
|
||||
@@ -74,7 +74,7 @@ describe('Follow-up 1 — no-address match winner reconciliation', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Follow-up 2 — match boot-recovery age guard', () => {
|
||||
describe('Follow-up 2: match boot-recovery age guard', () => {
|
||||
function queueWithCapturingDb() {
|
||||
const params = [];
|
||||
const db = {
|
||||
|
||||
@@ -405,12 +405,12 @@ describe('GameModeManager', () => {
|
||||
|
||||
describe('processCreditsPackageConfirmation', () => {
|
||||
test('adds correct credits from package info', async () => {
|
||||
// Step 1: db.query — SELECT payment lookup
|
||||
// Step 1: db.query, the SELECT payment lookup
|
||||
mockDb.query.mockResolvedValueOnce({
|
||||
rows: [{ user_id: 1, description: '', status: 'pending' }]
|
||||
});
|
||||
|
||||
// Step 2: withTransaction — client.query calls
|
||||
// Step 2: withTransaction, then client.query calls
|
||||
mockClient.query.mockImplementation(async (sql) => {
|
||||
if (/UPDATE payments/i.test(sql)) return { rows: [{ id: 123 }], rowCount: 1 };
|
||||
if (/SELECT id, premium_level/i.test(sql)) return { rows: [{ id: 1, premium_level: 'free' }], rowCount: 1 };
|
||||
@@ -590,12 +590,12 @@ describe('GameModeManager', () => {
|
||||
});
|
||||
|
||||
test('falls back to default credits when package info missing and no description', async () => {
|
||||
// Step 1: db.query — SELECT payment lookup with no credits in description
|
||||
// Step 1: db.query, the SELECT payment lookup with no credits in description
|
||||
mockDb.query.mockResolvedValueOnce({
|
||||
rows: [{ user_id: 1, description: 'Some payment', status: 'pending' }]
|
||||
});
|
||||
|
||||
// Step 2: withTransaction — client.query calls
|
||||
// Step 2: withTransaction, then client.query calls
|
||||
mockClient.query.mockImplementation(async (sql) => {
|
||||
if (/UPDATE payments/i.test(sql)) return { rows: [{ id: 123 }], rowCount: 1 };
|
||||
if (/SELECT id, premium_level/i.test(sql)) return { rows: [{ id: 1, premium_level: 'free' }], rowCount: 1 };
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Identity resolution tests (Phase 2.1).
|
||||
*
|
||||
* getOrCreateUser must resolve through the stable anon_token identity (via SessionManager)
|
||||
* rather than the mutable, non-unique socket_id — and must NOT create a duplicate "orphan"
|
||||
* rather than the mutable, non-unique socket_id, and must NOT create a duplicate "orphan"
|
||||
* user row when a session already exists.
|
||||
*/
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* matchRework_ultra — regression coverage for the match-mode production hardening pass.
|
||||
* matchRework_ultra: regression coverage for the match-mode production hardening pass.
|
||||
*
|
||||
* Exercises the confirmed defects the rework fixes: the move() crash (MP-C2), the real
|
||||
* countdown (MP-H6), reconnect re-keying + ownership (MP-H1/H2), winner consistency across
|
||||
@@ -43,7 +43,7 @@ function makeCryptoFinishedRoom({ winner = 'a' } = {}) {
|
||||
return room;
|
||||
}
|
||||
|
||||
describe('match rework — crash + countdown (MatchManager)', () => {
|
||||
describe('match rework: crash + countdown (MatchManager)', () => {
|
||||
const managers = [];
|
||||
function mgr(opts = {}) {
|
||||
const m = new MatchManager({ io: makeIo(), db: null, debugManager: null, ...opts });
|
||||
@@ -160,7 +160,7 @@ describe('match rework — crash + countdown (MatchManager)', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('match rework — reconnect re-keying + ownership (MP-H1/H2)', () => {
|
||||
describe('match rework: reconnect re-keying + ownership (MP-H1/H2)', () => {
|
||||
const managers = [];
|
||||
afterEach(() => { while (managers.length) managers.pop().shutdown(); delete process.env.MATCH_ENABLED; });
|
||||
|
||||
@@ -246,7 +246,7 @@ describe('match rework — reconnect re-keying + ownership (MP-H1/H2)', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('match rework — winner consistency across end paths', () => {
|
||||
describe('match rework: winner consistency across end paths', () => {
|
||||
test('all-dead: winnerId equals the placement #1 player (never a stranded null)', () => {
|
||||
const room = new MatchRoom({ economy: 'crypto_race', entrants: { a: { userId: 1 }, b: { userId: 2 } }, seed: SEED });
|
||||
room.start();
|
||||
@@ -269,7 +269,7 @@ describe('match rework — winner consistency across end paths', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('match rework — payout economics (MP-C4/H5)', () => {
|
||||
describe('match rework: payout economics (MP-C4/H5)', () => {
|
||||
function capturingDb({ address = 'WALLET', pot = '20000', fee = '1000' } = {}) {
|
||||
const captured = [];
|
||||
const amount = (BigInt(pot) - BigInt(fee)).toString();
|
||||
@@ -378,7 +378,7 @@ describe('match rework — payout economics (MP-C4/H5)', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('match rework — refunds on every cancellation path (MP-H3/H4)', () => {
|
||||
describe('match rework: refunds on every cancellation path (MP-H3/H4)', () => {
|
||||
afterEach(() => { delete process.env.MATCH_ENABLED; delete process.env.MATCH_CREDITS_COST; });
|
||||
|
||||
test('refundEntries cancels the queue row AND writes a ticket refund ledger row', async () => {
|
||||
@@ -447,7 +447,7 @@ describe('match rework — refunds on every cancellation path (MP-H3/H4)', () =>
|
||||
});
|
||||
});
|
||||
|
||||
describe('match rework — durable-finish isolated finalize (MP-C5)', () => {
|
||||
describe('match rework: durable-finish isolated finalize (MP-C5)', () => {
|
||||
afterEach(() => { delete process.env.MATCH_ENABLED; });
|
||||
|
||||
test('a nondurable winner creates no payout, leaderboard row, or final announcement', async () => {
|
||||
|
||||
@@ -100,7 +100,7 @@ describe('MatchScheduler', () => {
|
||||
await scheduler.onBlock(100);
|
||||
const { room } = manager.attached[0];
|
||||
// MP-H6: the scheduler creates the engine and hands it to the manager (setEngine) but
|
||||
// does NOT start it. The room stays in 'starting' — moves are rejected — until the
|
||||
// does NOT start it. The room stays in 'starting', moves are rejected, until the
|
||||
// manager's countdown elapses, so honest and modified clients always start together.
|
||||
expect(manager.engines.size).toBe(1);
|
||||
expect(room.status).toBe('starting');
|
||||
|
||||
@@ -43,7 +43,7 @@ describe('money.mulByDecimal (exact, no float)', () => {
|
||||
});
|
||||
|
||||
test('large amounts stay exact where float would drift', () => {
|
||||
// 9,007,199,254,740,993 is 2^53 + 1 — not representable as a float64.
|
||||
// 9,007,199,254,740,993 is 2^53 + 1: not representable as a float64.
|
||||
const big = 9007199254740993n;
|
||||
expect(money.mulByDecimal(big, 3)).toBe(27021597764222979n);
|
||||
// Sanity: the naive float computation is wrong, proving why BigInt matters.
|
||||
|
||||
@@ -19,18 +19,18 @@ describe('multi-level descent', () => {
|
||||
expect(game.maxDepth).toBe(3);
|
||||
expect(game.depth).toBe(1);
|
||||
expect(game.gameState).toBe('active');
|
||||
expect(game.dungeon.treasure).toBeNull(); // level 1/3 — no treasure
|
||||
expect(game.dungeon.treasure).toBeNull(); // level 1/3: no treasure
|
||||
|
||||
game._descend();
|
||||
expect(game.depth).toBe(2);
|
||||
expect(game.gameState).toBe('active'); // descending is NOT winning
|
||||
expect(game.dungeon.treasure).toBeNull(); // level 2/3 — still no treasure
|
||||
expect(game.dungeon.treasure).toBeNull(); // level 2/3: still no treasure
|
||||
expect(game.dungeon.entrance).toBeTruthy();
|
||||
expect(game.dungeon.exit).toBeTruthy();
|
||||
|
||||
game._descend();
|
||||
expect(game.depth).toBe(3);
|
||||
expect(game.dungeon.treasure).not.toBeNull(); // the vault — treasure present
|
||||
expect(game.dungeon.treasure).not.toBeNull(); // the vault: treasure present
|
||||
});
|
||||
|
||||
test('every level is reproducible from the committed seed + depth (provably fair)', () => {
|
||||
@@ -62,7 +62,7 @@ describe('multi-level descent', () => {
|
||||
game.player.moveTo(ex - 1, ey); // stand next to the exit on a floor cell if possible
|
||||
// Force the player adjacent by teleporting onto an exit-adjacent floor, then step in. If the
|
||||
// left neighbour isn't floor, just teleport onto the exit-1 and assert via a direct exit check.
|
||||
game.player.moveTo(ex, ey); // already on exit — re-derive win via a no-op-ish move
|
||||
game.player.moveTo(ex, ey); // already on exit: re-derive win via a no-op-ish move
|
||||
// A fresh move INTO the exit from a neighbour: place on a known floor neighbour of the exit.
|
||||
const map = game.dungeon.map;
|
||||
const nbrs = [[ex - 1, ey], [ex + 1, ey], [ex, ey - 1], [ex, ey + 1]];
|
||||
@@ -73,7 +73,7 @@ describe('multi-level descent', () => {
|
||||
expect(res.event).toBe('escaped');
|
||||
expect(game.gameState).toBe('won');
|
||||
} else {
|
||||
// Degenerate layout (exit fully walled) — the descent path is still covered by other tests.
|
||||
// Degenerate layout (exit fully walled): the descent path is still covered by other tests.
|
||||
expect(game.maxDepth).toBe(1);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -18,7 +18,7 @@ describe('per-network difficulty tuning (multi-level)', () => {
|
||||
test('tuning sets LEVELS ∝ block time, leaving size + monster at the preset', () => {
|
||||
const w = applyNetworkTuning(casino(), 'WOW');
|
||||
expect(w.levels).toBe(NETWORK_TUNING.WOW.levels);
|
||||
// Size and monster are untouched — depth is the pacing lever, not size/monster-speed.
|
||||
// Size and monster are untouched: depth is the pacing lever, not size/monster-speed.
|
||||
expect(w.dungeon.width).toBe(DIFFICULTY_PRESETS.casino.dungeon.width);
|
||||
expect(w.monster.movesPerPlayerMove).toBe(DIFFICULTY_PRESETS.casino.monster.movesPerPlayerMove);
|
||||
});
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
*
|
||||
* A run's lifetime is measured in blocks and its entry block comes from the last SUCCESSFUL poll.
|
||||
* With every node down that value goes stale silently, so a game admitted during an outage was
|
||||
* anchored to an already-past block and killed as a timeout on the first recovered poll — the
|
||||
* anchored to an already-past block and killed as a timeout on the first recovered poll: the
|
||||
* player lost a run, and in paid mode an entry, to our infrastructure. Refuse up front instead.
|
||||
*/
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Pillar 5 — global cross-server chat over nostr, layered on the ChatProvider seam.
|
||||
* Pillar 5: global cross-server chat over nostr, layered on the ChatProvider seam.
|
||||
*/
|
||||
const NostrChatProvider = require('../src/network/chat/NostrChatProvider');
|
||||
const { buildChatProvider } = require('../src/network/chat');
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Pack registry — the engine for multi-pack, interchangeable render environments (products).
|
||||
* Pack registry: the engine for multi-pack, interchangeable render environments (products).
|
||||
*/
|
||||
describe('pack registry', () => {
|
||||
let RK;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
const { buildProviderRegistry, parseRouting } = require('../src/payments/providers');
|
||||
|
||||
const fakeWallet = { createPaymentRequest: async () => ({}), checkPaymentStatus: async () => ({}), processBatchPayout: async () => ({}) };
|
||||
const CHECKOUT = { // full LAN infra: real BTCPay (BTC/LTC) + xmrcheckout + wowcheckout
|
||||
BTCPAY_URL: 'http://10.42.1.33', BTCPAY_STORE_ID: 'b', BTCPAY_API_KEY: 'k',
|
||||
XMRCHECKOUT_URL: 'http://10.42.1.33:8080', XMRCHECKOUT_STORE_ID: 'x', XMRCHECKOUT_API_KEY: 'k',
|
||||
WOWCHECKOUT_URL: 'http://10.42.1.33:8180', WOWCHECKOUT_STORE_ID: 'w', WOWCHECKOUT_API_KEY: 'k'
|
||||
const CHECKOUT = { // full provider set (BTC/LTC) + xmrcheckout + wowcheckout
|
||||
BTCPAY_URL: 'http://192.0.2.33', BTCPAY_STORE_ID: 'b', BTCPAY_API_KEY: 'k',
|
||||
XMRCHECKOUT_URL: 'http://192.0.2.33:8080', XMRCHECKOUT_STORE_ID: 'x', XMRCHECKOUT_API_KEY: 'k',
|
||||
WOWCHECKOUT_URL: 'http://192.0.2.33:8180', WOWCHECKOUT_STORE_ID: 'w', WOWCHECKOUT_API_KEY: 'k'
|
||||
};
|
||||
|
||||
describe('parseRouting', () => {
|
||||
@@ -17,7 +17,7 @@ describe('parseRouting', () => {
|
||||
});
|
||||
|
||||
describe('buildProviderRegistry', () => {
|
||||
test('native-only (no gateway env): every chain routes to native — behavior-preserving', () => {
|
||||
test('native-only (no gateway env): every chain routes to native, behavior-preserving', () => {
|
||||
const reg = buildProviderRegistry({ env: {}, walletService: fakeWallet });
|
||||
expect(reg.getProvider('WOW').id).toBe('native-monero');
|
||||
expect(reg.getProvider('XMR').id).toBe('native-monero');
|
||||
@@ -45,7 +45,7 @@ describe('buildProviderRegistry', () => {
|
||||
|
||||
test('a checkout gateway wins over the native wallet for its chain', () => {
|
||||
const reg = buildProviderRegistry({
|
||||
env: { XMRCHECKOUT_URL: 'http://10.42.1.33:8080', XMRCHECKOUT_STORE_ID: 'x', XMRCHECKOUT_API_KEY: 'k' },
|
||||
env: { XMRCHECKOUT_URL: 'http://192.0.2.33:8080', XMRCHECKOUT_STORE_ID: 'x', XMRCHECKOUT_API_KEY: 'k' },
|
||||
walletService: fakeWallet
|
||||
});
|
||||
expect(reg.getProvider('XMR').id).toBe('xmrcheckout');
|
||||
|
||||
@@ -162,6 +162,6 @@ describe('NativeMoneroProvider wraps walletRPCService', () => {
|
||||
expect(w.startPaymentMonitoring).toHaveBeenCalledWith('Wo3xaddr', expect.any(Function), 2000);
|
||||
const raw = { in_mempool: true, confirmed: false, complete: false, amount: 42, required: 100, confirmations: 0 };
|
||||
captured(raw);
|
||||
expect(seen[0]).toBe(raw); // same object, not normalized — callback sees exactly what the wallet emitted
|
||||
expect(seen[0]).toBe(raw); // same object, not normalized: callback sees exactly what the wallet emitted
|
||||
});
|
||||
});
|
||||
|
||||
@@ -53,7 +53,7 @@ const ONE_PENDING = [
|
||||
{ id: 42, user_id: 1, game_id: 7, payout_address: 'addrA', amount: '100000000000', multiplier: 2, reason: 'escape' }
|
||||
];
|
||||
|
||||
describe('M1 — single payout ambiguous failure', () => {
|
||||
describe('M1: single payout ambiguous failure', () => {
|
||||
test('is marked needs_review (never failed) and alerts', async () => {
|
||||
const walletService = {
|
||||
getBalance: jest.fn().mockResolvedValue({ unlocked_balance: '10000000000000' }),
|
||||
@@ -95,7 +95,7 @@ describe('M1 — single payout ambiguous failure', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('M3 — atomic single_game payment claim', () => {
|
||||
describe('M3: atomic single_game payment claim', () => {
|
||||
test('aborts cleanly when the payment was already consumed (claim returns no row)', async () => {
|
||||
const { gmm } = buildGmm();
|
||||
// withTransaction runs cb with a client whose claim SELECT returns no rows.
|
||||
@@ -125,7 +125,7 @@ describe('M3 — atomic single_game payment claim', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('M4 — completeGame durably records every committed liability', () => {
|
||||
describe('M4: completeGame durably records every committed liability', () => {
|
||||
test('quarantines a liability when the paying identity cannot be resolved', async () => {
|
||||
const { gmm, db } = buildGmm();
|
||||
db.withTransaction.mockImplementation(async (cb) => cb(db));
|
||||
@@ -145,7 +145,7 @@ describe('M4 — completeGame durably records every committed liability', () =>
|
||||
});
|
||||
});
|
||||
|
||||
describe('M5 — payout amount is clamped to configured bounds', () => {
|
||||
describe('M5: payout amount is clamped to configured bounds', () => {
|
||||
test('caps an over-max payout and alerts', async () => {
|
||||
const { gmm, db } = buildGmm();
|
||||
db.withTransaction.mockImplementation(async (cb) => cb(db));
|
||||
@@ -188,7 +188,7 @@ describe('M5 — payout amount is clamped to configured bounds', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('C1 — getOrCreateUser create:false', () => {
|
||||
describe('C1: getOrCreateUser create:false', () => {
|
||||
test('returns null and never inserts when no user row exists', async () => {
|
||||
const { gmm, db } = buildGmm();
|
||||
db.query.mockResolvedValue({ rows: [] }); // socket lookup finds nothing
|
||||
|
||||
@@ -126,7 +126,7 @@ describe('Payment flow integration: confirm-after-block immediate start', () =>
|
||||
expect(queueManager.getQueueLength()).toBe(1);
|
||||
|
||||
// Confirmation after the block. `complete: true` means the received amount covers
|
||||
// the expected amount — required for a game to start (Phase 0.1 gate).
|
||||
// the expected amount: required for a game to start (Phase 0.1 gate).
|
||||
await monitorCallback({ in_mempool: true, confirmed: true, complete: true, amount: 1000, required: 1000, confirmations: 1 });
|
||||
|
||||
const gameStarts = events.filter(e => e.event === 'game_start');
|
||||
|
||||
@@ -239,12 +239,12 @@ describe('Payment & Payout Comprehensive Tests', () => {
|
||||
|
||||
describe('Credits Package Confirmation', () => {
|
||||
test('adds credits from package info correctly', async () => {
|
||||
// Step 1: db.query — SELECT payment lookup (has user_id, status='pending')
|
||||
// Step 1: db.query runs the SELECT payment lookup (has user_id, status='pending')
|
||||
mockDb.query.mockResolvedValueOnce({
|
||||
rows: [{ user_id: 1, description: '', status: 'pending' }]
|
||||
});
|
||||
|
||||
// Step 2: withTransaction — client.query calls:
|
||||
// Step 2: withTransaction runs these client.query calls:
|
||||
mockDb._mockClient.query
|
||||
.mockResolvedValueOnce({ rows: [{ id: 123, status: 'pending' }] }) // snapshot product promise
|
||||
.mockResolvedValueOnce({ rows: [{ id: 123 }] }) // mark payment confirmed
|
||||
@@ -262,12 +262,12 @@ describe('Payment & Payout Comprehensive Tests', () => {
|
||||
});
|
||||
|
||||
test('parses credits from payment description as fallback', async () => {
|
||||
// Step 1: db.query — SELECT payment lookup with description
|
||||
// Step 1: db.query runs the SELECT payment lookup with description
|
||||
mockDb.query.mockResolvedValueOnce({
|
||||
rows: [{ user_id: 1, description: 'Wowngeon 25 credits package (WOW)', status: 'pending' }]
|
||||
});
|
||||
|
||||
// Step 2: withTransaction — client.query calls:
|
||||
// Step 2: withTransaction runs these client.query calls:
|
||||
mockDb._mockClient.query
|
||||
.mockResolvedValueOnce({ rows: [{ id: 123, status: 'pending' }] })
|
||||
.mockResolvedValueOnce({ rows: [{ id: 123 }] })
|
||||
@@ -284,12 +284,12 @@ describe('Payment & Payout Comprehensive Tests', () => {
|
||||
});
|
||||
|
||||
test('defaults to 10 credits when no info available', async () => {
|
||||
// Step 1: db.query — SELECT payment lookup with non-matching description
|
||||
// Step 1: db.query runs the SELECT payment lookup with non-matching description
|
||||
mockDb.query.mockResolvedValueOnce({
|
||||
rows: [{ user_id: 1, description: 'Some payment', status: 'pending' }]
|
||||
});
|
||||
|
||||
// Step 2: withTransaction — client.query calls:
|
||||
// Step 2: withTransaction runs these client.query calls:
|
||||
mockDb._mockClient.query
|
||||
.mockResolvedValueOnce({ rows: [{ id: 123, status: 'pending' }] })
|
||||
.mockResolvedValueOnce({ rows: [{ id: 123 }] })
|
||||
@@ -342,7 +342,7 @@ describe('Payment & Payout Comprehensive Tests', () => {
|
||||
expect(result.payout.payoutId).toBe(999);
|
||||
expect(result.payout.amount).toBe(200000000000);
|
||||
expect(result.payout.multiplier).toBe(2);
|
||||
// processPayout is NOT called directly — payout is batched via _scheduleBatchPayout
|
||||
// processPayout is NOT called directly: payout is batched via _scheduleBatchPayout
|
||||
expect(mockWalletService.processPayout).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Phase 2 — per-player nostr identity: verify client-signed chat events, relay them under the
|
||||
* Phase 2, per-player nostr identity: verify client-signed chat events, relay them under the
|
||||
* player's own npub, and let a premium subscription unlock cosmetic tiers.
|
||||
*/
|
||||
const { verifyChatEvent } = require('../src/utils/verifyChatEvent');
|
||||
@@ -101,7 +101,7 @@ describe('relaySignedEvent', () => {
|
||||
});
|
||||
|
||||
describe('premium subscription unlocks cosmetic tiers', () => {
|
||||
test('an active premium sub sets the tier and unlocks tier-1 packs — no purchase needed', () => {
|
||||
test('an active premium sub sets the tier and unlocks tier-1 packs: no purchase needed', () => {
|
||||
const free = Entitlements.snapshotForUser({ credits: 0, total_credits_purchased: 0 });
|
||||
expect(free.tier).toBe(0);
|
||||
expect(free.packs['generated-skins']).toBe(false); // tier-1 pack locked for free
|
||||
|
||||
@@ -29,7 +29,7 @@ describe('Provably fair: deterministic dungeon generation', () => {
|
||||
|
||||
test('regeneration is stable even when interleaved with another seed (no global RNG bleed)', () => {
|
||||
const a1 = DungeonGenerator.layoutFingerprint(DungeonGenerator.regenerateFromSeed(SEED_A, 'WOW'));
|
||||
// Generate a different seed's dungeon in between — must not affect SEED_A's output.
|
||||
// Generate a different seed's dungeon in between: must not affect SEED_A's output.
|
||||
DungeonGenerator.regenerateFromSeed(SEED_B, 'WOW');
|
||||
const a2 = DungeonGenerator.layoutFingerprint(DungeonGenerator.regenerateFromSeed(SEED_A, 'WOW'));
|
||||
expect(a1).toBe(a2);
|
||||
|
||||
@@ -14,7 +14,7 @@ function buildManager() {
|
||||
debugManager: { getCurrentBlockHeight: () => 10 },
|
||||
broadcastManager: {},
|
||||
io,
|
||||
createGameForUser: jest.fn(async (_user, _type, options) => { captured.push(options); return game; }),
|
||||
createGameForUser: jest.fn(async (_user, options) => { captured.push(options); return game; }),
|
||||
getUserBySocket: () => user,
|
||||
activeGames: new Map(),
|
||||
gameModeManager: { processGameStart: jest.fn().mockResolvedValue({ success: true }) },
|
||||
|
||||
@@ -146,7 +146,7 @@ describe('render pack visual resolver', () => {
|
||||
|
||||
// With test unlocks on, ALL premium modes and packs are available for QA
|
||||
RK.RENDER_MODE_TEST_UNLOCKS = true;
|
||||
expect(RK.canUseMode('iso')).toBe(true); // 'fancy' is no longer a mode — it's a topdown pack
|
||||
expect(RK.canUseMode('iso')).toBe(true); // 'fancy' is no longer a mode; it's a topdown pack
|
||||
expect(RK.canUseMode('3d')).toBe(true);
|
||||
expect(RK.canUsePack('iso-dungeon')).toBe(true);
|
||||
expect(RK.canUsePack('kenney-3d-characters')).toBe(true);
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* Two defects motivated this:
|
||||
* - the stock configuration pointed PRIMARY and FALLBACK at the SAME host, so what looked like
|
||||
* redundancy was none, and only two endpoints were ever supported;
|
||||
* - `failoverActive` was a one-way latch — once the process moved to a backup it stayed there
|
||||
* - `failoverActive` was a one-way latch: once the process moved to a backup it stayed there
|
||||
* until restart, even after the preferred node recovered.
|
||||
*
|
||||
* The contract now: one logical call tries every configured node before failing, the preferred node
|
||||
@@ -46,7 +46,7 @@ describe('daemon endpoint failover', () => {
|
||||
expect(svc.failoverActive).toBe(false);
|
||||
});
|
||||
|
||||
test('deduplicates endpoints — the stock PRIMARY == FALLBACK config is one node, not two', () => {
|
||||
test('deduplicates endpoints: the stock PRIMARY == FALLBACK config is one node, not two', () => {
|
||||
const same = 'http://127.0.0.1:34568';
|
||||
const svc = new RpcService({
|
||||
http: httpFor([same]),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Pillar 4 — Ruleset engine + PvP modes on the match engine.
|
||||
* Pillar 4: Ruleset engine + PvP modes on the match engine.
|
||||
*/
|
||||
const { defineRuleset, WIN, getRuleset, listRulesets, rulesetFromMatchOpts } = require('../src/game/rulesets');
|
||||
const MatchRoom = require('../src/multiplayer/MatchRoom');
|
||||
@@ -179,7 +179,7 @@ describe('MatchRoom drives new rulesets', () => {
|
||||
]));
|
||||
});
|
||||
|
||||
test('high-score: reaching the exit does NOT instantly win — the match keeps going', () => {
|
||||
test('high-score: reaching the exit does NOT instantly win; the match keeps going', () => {
|
||||
const room = new MatchRoom({
|
||||
ruleset: { id: 'test-score', winCondition: { type: WIN.HIGH_SCORE }, entities: { monster: false }, players: { min: 1, max: 4 } },
|
||||
entrants: { a: { userId: 1 }, b: { userId: 2 } },
|
||||
@@ -191,7 +191,7 @@ describe('MatchRoom drives new rulesets', () => {
|
||||
const res = room.resolveTick();
|
||||
|
||||
expect(room.playerStates.get('a').escaped).toBe(true);
|
||||
expect(room.status).toBe('active'); // b is still in play — no instant win
|
||||
expect(room.status).toBe('active'); // b is still in play: no instant win
|
||||
expect(room.winnerId).toBeNull();
|
||||
expect(res.finished).toBe(false);
|
||||
|
||||
|
||||
@@ -147,7 +147,7 @@ describe('SQL Injection Prevention', () => {
|
||||
// Only attempt a real DB connection when a usable password is configured. Without one,
|
||||
// node-postgres throws inside the SASL handshake *after* opening the TCP socket and
|
||||
// orphans that socket (it survives pool.end()), which would keep Jest from exiting. A
|
||||
// missing password simply means "DB not available" here — and these tests already fall
|
||||
// missing password simply means "DB not available" here, and these tests already fall
|
||||
// back to mocks in that case.
|
||||
const hasDbPassword = typeof process.env.DB_PASSWORD === 'string' && process.env.DB_PASSWORD.length > 0;
|
||||
if (hasDbPassword) {
|
||||
@@ -273,7 +273,7 @@ describe('SQL Injection Prevention', () => {
|
||||
const firstToken = created.token;
|
||||
expect(firstToken).toBeTruthy();
|
||||
|
||||
// Resume with that token — should succeed and hand back a DIFFERENT token.
|
||||
// Resume with that token: should succeed and hand back a DIFFERENT token.
|
||||
const resumed = await sessionManager.resumeOrCreate({
|
||||
socketId: 'rotate-socket-2',
|
||||
ipAddress: '127.0.0.1',
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* `Game.getState()` used to put the absolute entrance/exit/treasure coordinates AND the monster's
|
||||
* exact position into every `game_start` / `game_update` frame, relying entirely on the browser
|
||||
* renderer to decline to draw them. Anyone reading the socket payload in devtools could walk
|
||||
* straight to the treasure and the exit from move 0 — in a paid mode, a direct attack on the payout,
|
||||
* straight to the treasure and the exit from move 0: in a paid mode, a direct attack on the payout,
|
||||
* and the same frame is forwarded verbatim to spectators.
|
||||
*
|
||||
* Concealment is now enforced where it is authoritative: the server. Discovery is STICKY so a
|
||||
@@ -48,7 +48,7 @@ describe('server-side fog of war', () => {
|
||||
const state = game.getState();
|
||||
expect(state.exit).toBeNull();
|
||||
expect(state.treasure).toBeNull();
|
||||
// The dungeon itself still knows — only the wire frame is redacted.
|
||||
// The dungeon itself still knows; only the wire frame is redacted.
|
||||
expect(game.dungeon.exit).toEqual(expect.any(Array));
|
||||
});
|
||||
|
||||
@@ -81,7 +81,7 @@ describe('server-side fog of war', () => {
|
||||
expect(seen.x).toBe(game.monster.x);
|
||||
expect(seen.y).toBe(game.monster.y);
|
||||
|
||||
// Unlike features, the monster moves — concealment is NOT sticky, or you could track the
|
||||
// Unlike features, the monster moves: concealment is NOT sticky, or you could track the
|
||||
// hunter through walls for the rest of the run.
|
||||
setVisible(game, game.monster, false);
|
||||
expect(game.getState().monster).toBeNull();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Guards the balance-sim harness: it must keep driving the REAL engine to terminal outcomes and
|
||||
// producing well-formed, in-range statistics. Not a balance assertion (those numbers are meant to
|
||||
// move) — a structural regression guard so the instrument can't silently rot.
|
||||
// move), a structural regression guard so the instrument can't silently rot.
|
||||
const { runOneGame, simulatePreset } = require('../src/sim/simulate');
|
||||
const { BOTS } = require('../src/sim/simBots');
|
||||
const { bfsField, stepDownField } = require('../src/sim/pathfind');
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
*
|
||||
* If a user pays for a single game and disconnects before a game starts (no games row
|
||||
* references the payment), the money was taken with nothing given. recoverPendingPayments
|
||||
* must grant the equivalent credits — exactly once (idempotent), and never for a payment
|
||||
* must grant the equivalent credits, exactly once (idempotent), and never for a payment
|
||||
* that was actually consumed by a game.
|
||||
*/
|
||||
|
||||
@@ -12,7 +12,7 @@ const SessionManager = require('../src/network/sessionManager');
|
||||
function makeDb({ singleGamePayments = [], gamesForPayment = new Set(), recoveredReasons = new Set(), entitlementPaymentIds = new Set() }) {
|
||||
let credits = 0;
|
||||
const handler = async (text, params = []) => {
|
||||
// credits_package recovery queries (unprocessed + orphaned) — none in these tests
|
||||
// credits_package recovery queries (unprocessed + orphaned): none in these tests
|
||||
if (/FROM payments[\s\S]*credits_package/i.test(text)) return { rows: [] };
|
||||
if (/FROM payments\s+WHERE id = \$1[\s\S]*FOR UPDATE/i.test(text)) {
|
||||
const p = singleGamePayments.find(row => row.id === params[0]);
|
||||
@@ -84,7 +84,7 @@ describe('Single-game disconnect recovery', () => {
|
||||
expect(db._getCredits()).toBe(0);
|
||||
});
|
||||
|
||||
test('is idempotent — a second recovery does not double-credit', async () => {
|
||||
test('is idempotent: a second recovery does not double-credit', async () => {
|
||||
const db = makeDb({ singleGamePayments: [{ id: 42, confirmed_at: new Date() }] });
|
||||
const sm = makeSession(db);
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ describe('single-player character identity wiring', () => {
|
||||
});
|
||||
|
||||
test('welcome legend draws the player as the original hero tile (uniform grid, no overlay)', () => {
|
||||
// The splash draws every icon — including the player — as a grid tile so the four rows align
|
||||
// The splash draws every icon, including the player, as a grid tile so the four rows align
|
||||
// and the player is the original hero tile, not a staggered avatar overlay.
|
||||
expect(screenManager).toContain('role: "player"');
|
||||
expect(screenManager).toContain('SinglePlayerAvatar.clearOverlay'); // overlay still cleared
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* We sign REAL kind:27235 events with nostr-tools (generateSecretKey / getPublicKey /
|
||||
* finalizeEvent) and then attack them: tamper content/tags, wrong kind/method/url,
|
||||
* stale timestamps, flipped signatures, and the memoization-bypass trick. The verifier
|
||||
* is exercised directly (no HTTP). Case (ix) — single-use / replay — is enforced at the
|
||||
* is exercised directly (no HTTP). Case (ix), single-use / replay, is enforced at the
|
||||
* route layer, so we drive the real Express route with a mock DB for that one.
|
||||
*/
|
||||
|
||||
@@ -72,7 +72,7 @@ function baseOpts(extra = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
describe('verifyNip98Event — adversarial round-trip', () => {
|
||||
describe('verifyNip98Event: adversarial round-trip', () => {
|
||||
test('(i) a valid, correctly-signed event is accepted and returns the x-only pubkey', () => {
|
||||
const sk = generateSecretKey();
|
||||
const ev = buildEvent(sk);
|
||||
@@ -120,7 +120,7 @@ describe('verifyNip98Event — adversarial round-trip', () => {
|
||||
expect(r.reason).toBe('wrong-kind');
|
||||
});
|
||||
|
||||
test('(v) an expired created_at (beyond maxSkew) is rejected — past and future', () => {
|
||||
test('(v) an expired created_at (beyond maxSkew) is rejected: past and future', () => {
|
||||
const sk = generateSecretKey();
|
||||
const stale = buildEvent(sk, { created_at: NOW - 10_000 });
|
||||
const past = verifyNip98Event(stale, baseOpts({ now: NOW }));
|
||||
@@ -189,7 +189,7 @@ describe('verifyNip98Event — adversarial round-trip', () => {
|
||||
expect(verifyNip98Event({}, baseOpts()).ok).toBe(false);
|
||||
const sk = generateSecretKey();
|
||||
const ev = buildEvent(sk);
|
||||
// Corrupt the pubkey to non-hex — must not throw, must reject.
|
||||
// Corrupt the pubkey to non-hex: must not throw, must reject.
|
||||
const bad = { ...ev, pubkey: 'zz' + ev.pubkey.slice(2) };
|
||||
const r = verifyNip98Event(bad, baseOpts());
|
||||
expect(r.ok).toBe(false);
|
||||
@@ -218,14 +218,14 @@ describe('verifyNip98Event — adversarial round-trip', () => {
|
||||
expect(verifyEvent(ev)).toBe(true);
|
||||
|
||||
// Our verifier rebuilds a clean event (no memo symbol) and re-runs schnorr, so the
|
||||
// signature — made over the ORIGINAL content — fails against the repaired id.
|
||||
// signature, made over the ORIGINAL content, fails against the repaired id.
|
||||
const r = verifyNip98Event(ev, baseOpts());
|
||||
expect(r.ok).toBe(false);
|
||||
expect(r.reason).toBe('bad-signature');
|
||||
});
|
||||
});
|
||||
|
||||
describe('(ix) route layer — single-use challenge consume rejects replay', () => {
|
||||
describe('(ix) route layer: single-use challenge consume rejects replay', () => {
|
||||
const express = appRequire('express');
|
||||
const http = require('http');
|
||||
const createAuthRoutes = require('../src/routes/auth');
|
||||
@@ -318,7 +318,7 @@ describe('(ix) route layer — single-use challenge consume rejects replay', ()
|
||||
const socketId = 'sock-xyz-1';
|
||||
const token = 'route-session-token';
|
||||
const challenge = 'route-nonce-9f8e7d';
|
||||
// created_at must be genuinely fresh — the route uses real Date.now().
|
||||
// created_at must be genuinely fresh: the route uses real Date.now().
|
||||
const ev = buildEvent(sk, { created_at: Math.floor(Date.now() / 1000), challenge });
|
||||
|
||||
const db = mockDb(challenge, socketId, token);
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
*
|
||||
* Socket.IO invokes listeners WITHOUT a try/catch, and this process treats `uncaughtException` and
|
||||
* `unhandledRejection` as fatal (src/index.js calls gracefulShutdown and exits). That combination
|
||||
* made any unguarded throw in any listener an unauthenticated remote kill switch — the original
|
||||
* made any unguarded throw in any listener an unauthenticated remote kill switch: the original
|
||||
* instance was `socket.emit('debug_ping')` with no argument, which dereferenced `data.time` on
|
||||
* `undefined` and took the whole server down. systemd restarts it, the attacker repeats.
|
||||
*
|
||||
@@ -26,7 +26,7 @@ function fakeSocket() {
|
||||
}
|
||||
|
||||
// The wrapper is a closure created per connection inside handleConnection, which needs the full
|
||||
// dependency graph. Rebuild the identical shape here and assert its behaviour directly — the source
|
||||
// dependency graph. Rebuild the identical shape here and assert its behaviour directly: the source
|
||||
// of truth is that handleConnection registers every listener through it (asserted below).
|
||||
function makeSafeDispatch(handlers, socket) {
|
||||
return (event, handler) => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// The single-player render-kit migration hinges on sceneFromGameState consuming the CLIENT state
|
||||
// shape — sparse {y:{x:v}} tile maps (vs the server's arrays). Guards that adapter + the RK alias.
|
||||
// shape: sparse {y:{x:v}} tile maps (vs the server's arrays). Guards that adapter + the RK alias.
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const vm = require('vm');
|
||||
|
||||
@@ -17,7 +17,7 @@ describe('SubscriptionService', () => {
|
||||
expect(await svc.tierForNpub(pubHex)).toBeNull();
|
||||
});
|
||||
|
||||
test('PREMIUM_NPUBS allowlist grants the tier — hex and npub1 both match the stored pubkey', async () => {
|
||||
test('PREMIUM_NPUBS allowlist grants the tier: hex and npub1 both match the stored pubkey', async () => {
|
||||
const byHex = new SubscriptionService({ env: { PREMIUM_NPUBS: pubHex } });
|
||||
expect(await byHex.tierForNpub(pubHex)).toBe('premium'); // default tier
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
const Room = require('../src/multiplayer/Room');
|
||||
const Occupant = require('../src/multiplayer/Occupant');
|
||||
|
||||
describe('Room — shared real-time engine (Tavern instance)', () => {
|
||||
describe('Room: shared real-time engine (Tavern instance)', () => {
|
||||
test('builds a rectangular walkable map with at least one spawn', () => {
|
||||
const room = new Room({ id: 'test-room', type: 'tavern' });
|
||||
expect(room.rows).toBeGreaterThan(0);
|
||||
|
||||
@@ -14,7 +14,7 @@ describe('renderVerifyPage social card meta', () => {
|
||||
ogImage: 'og-card-xmr.png'
|
||||
});
|
||||
expect(html).toContain('property="og:title"');
|
||||
expect(html).toContain('Monero Roguelike — Dungeon Layout Verification');
|
||||
expect(html).toContain('Monero Roguelike: Dungeon Layout Verification');
|
||||
expect(html).toContain('<meta property="og:image" content="https://monerogue.app/og-card-xmr.png">');
|
||||
expect(html).toContain('content="summary_large_image"');
|
||||
expect(html).toContain('<meta property="og:url" content="https://monerogue.app/verify/seedABC">');
|
||||
|
||||
@@ -1,150 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { spawnSync } = require('child_process');
|
||||
|
||||
const rootPath = (...parts) => path.join(__dirname, '..', ...parts);
|
||||
const helperPath = rootPath('scripts', 'deploy', 'wallet-keyfile-recovery-proof.py');
|
||||
const fixturePath = rootPath('scripts', 'deploy', 'wallet-keyfile-recovery-fixture-test.py');
|
||||
const runbookPath = rootPath('docs', 'WALLET_KEYFILE_RECOVERY_PROOF.md');
|
||||
|
||||
describe('fixed wallet keyfile/password recovery proof boundary', () => {
|
||||
const helper = fs.readFileSync(helperPath, 'utf8');
|
||||
const runbook = fs.readFileSync(runbookPath, 'utf8');
|
||||
|
||||
test('accepts only the two audited legacy profiles and fixed recovery artifacts', () => {
|
||||
for (const expected of [
|
||||
'monero-wallet-rpc.service',
|
||||
'/usr/bin/monero-wallet-rpc',
|
||||
'/home/jw/Programs/monero-x86_64-linux-gnu-v0.18.4.4/test',
|
||||
'monero-stagenet-preprod-20260721T1140Z.wallet',
|
||||
'monero-stagenet-preprod-20260721T1140Z.keys',
|
||||
'/run/wowngeon-wallet-keyfile-proof-monero-stagenet',
|
||||
'wownero-wallet-rpc.service',
|
||||
'/usr/bin/wownero-wallet-rpc',
|
||||
'/home/jw/Programs/wow-11.3.0/game',
|
||||
'wownero-mainnet-preprod-20260721T1140Z.wallet',
|
||||
'wownero-mainnet-preprod-20260721T1140Z.keys',
|
||||
'/run/wowngeon-wallet-keyfile-proof-wownero-mainnet',
|
||||
]) {
|
||||
expect(helper).toContain(expected);
|
||||
}
|
||||
expect(helper).toContain('live_rpc_port=38083');
|
||||
expect(helper).toContain('proof_port=39084');
|
||||
expect(helper).toContain('live_rpc_port=34570');
|
||||
expect(helper).toContain('proof_port=39570');
|
||||
expect(helper).toContain('if len(arguments) != 1 or arguments[0] not in PROFILES:');
|
||||
expect(helper).not.toContain('argparse');
|
||||
});
|
||||
|
||||
test('validates live identity before extracting and wiping the exact inline password', () => {
|
||||
expect(helper).toContain('systemctl("is-active", "--quiet", profile.unit)');
|
||||
expect(helper).toContain('process_executable(pid) != profile.binary');
|
||||
expect(helper).toContain('process_executable_identity(pid) != expected_binary');
|
||||
expect(helper).toContain('process_has_unit_cgroup(pid, profile.unit)');
|
||||
expect(helper).toContain('require_exact_listener(');
|
||||
expect(helper).toContain('b"--wallet-file"');
|
||||
expect(helper).toContain('b"--password"');
|
||||
expect(helper).toContain('f"/proc/{pid}/cmdline"');
|
||||
expect(helper).toContain('os.readv(descriptor, [buffer])');
|
||||
expect(helper).toContain('wipe_buffer(buffer, buffer_is_protected)');
|
||||
expect(helper).toContain('PR_SET_DUMPABLE');
|
||||
expect(helper).toContain('PR_SET_NO_NEW_PRIVS');
|
||||
expect(helper).toContain('MADV_DONTDUMP');
|
||||
expect(helper).not.toMatch(/systemctl\([^\n]*(?:stop|start|restart|reload|enable|disable)/);
|
||||
});
|
||||
|
||||
test('drops candidate privilege and uses only fixed read/proof RPC methods', () => {
|
||||
expect(helper).toContain('os.setgroups([])');
|
||||
expect(helper).toContain('os.setgid(expected_gid)');
|
||||
expect(helper).toContain('os.setuid(expected_uid)');
|
||||
expect(helper).toContain('os.getresuid() != (expected_uid, expected_uid, expected_uid)');
|
||||
expect(helper).toContain('os.getresgid() != (expected_gid, expected_gid, expected_gid)');
|
||||
expect(helper).toContain('if process_groups(process.pid):');
|
||||
expect(helper).toContain('process_status_integer(process.pid, "NoNewPrivs") != 1');
|
||||
expect(helper).toContain('("CapInh", "CapPrm", "CapEff", "CapAmb")');
|
||||
expect(helper).toContain('LIBC.prctl(PR_SET_KEEPCAPS, 0, 0, 0, 0)');
|
||||
expect(helper).toContain('publish_candidate_tree(profile, expected_uid, expected_gid)');
|
||||
expect(helper).toContain('"method": "get_address"');
|
||||
expect(helper).toContain('rpc_request("sign", {"data": SIGN_MESSAGE})');
|
||||
expect(helper).toContain('"verify",');
|
||||
expect(helper).toContain('signature_type not in (None, "spend")');
|
||||
expect(helper).toContain('"--offline"');
|
||||
expect(helper).toContain('"--log-file"');
|
||||
expect(helper).toContain('"/dev/null"');
|
||||
expect(helper).toContain('password-file=');
|
||||
expect(helper).toContain('rpc-bind-ip=127.0.0.1');
|
||||
expect(helper).toContain('rpc-login=');
|
||||
expect(helper).toContain('if status != 401:');
|
||||
expect(helper).toContain('if status != 200:');
|
||||
expect(helper).toContain('hmac.compare_digest(live_address, restored_address)');
|
||||
for (const forbiddenMethod of [
|
||||
'query_key',
|
||||
'get_seed',
|
||||
'get_spend_key',
|
||||
'get_view_key',
|
||||
'transfer_split',
|
||||
'sweep_all',
|
||||
]) {
|
||||
expect(helper).not.toContain(`"method": "${forbiddenMethod}"`);
|
||||
}
|
||||
});
|
||||
|
||||
test('fails closed around source links, races, port collisions, and exact cleanup', () => {
|
||||
expect(helper).toContain('details.st_nlink != 1');
|
||||
expect(helper).toContain('O_NOFOLLOW');
|
||||
expect(helper).toContain('file_identity(os.fstat(source_fd)) != expected_source');
|
||||
expect(helper).toContain('refuse("proof-port-collision")');
|
||||
expect(helper).toContain('secure_remove_proof_directory(');
|
||||
expect(helper).toContain('(details.st_dev, details.st_ino) != expected_identity');
|
||||
expect(helper).toContain('result=passed');
|
||||
expect(helper).toContain('result=refused');
|
||||
expect(helper).not.toMatch(/(?:import traceback|traceback\.)/);
|
||||
expect(helper).not.toMatch(/\bprint\(/);
|
||||
});
|
||||
|
||||
test('runbook keeps seed custody and off-host restore explicitly unproven', () => {
|
||||
expect(runbook).toContain('has not been installed on or run against');
|
||||
expect(runbook).toContain('This is **not seed custody**');
|
||||
expect(runbook).toContain('it is **not an off-host disaster-recovery test**');
|
||||
expect(runbook).toMatch(/record seed custody and off-host recovery as\s+\*\*unproven\*\*/);
|
||||
expect(runbook).toContain('The proof candidate is offline');
|
||||
expect(runbook).toMatch(/Wallet-RPC never runs\s+as root/);
|
||||
expect(runbook).toContain('not from an independent custody source');
|
||||
expect(runbook).toContain('**not** an RPC-hardening or');
|
||||
expect(runbook).toContain('not evidence they have been run');
|
||||
expect(runbook).toContain('do not blindly remove it');
|
||||
});
|
||||
|
||||
test('synthetic fixture passes without opening a real wallet', () => {
|
||||
const result = spawnSync('/usr/bin/python3', [fixturePath], {
|
||||
encoding: 'utf8',
|
||||
timeout: 15000,
|
||||
});
|
||||
expect({ status: result.status, signal: result.signal, stdout: result.stdout }).toEqual({
|
||||
status: 0,
|
||||
signal: null,
|
||||
stdout: '',
|
||||
});
|
||||
expect(result.stderr).toContain('OK');
|
||||
});
|
||||
|
||||
test('invalid invocation renders only the refusal status', () => {
|
||||
const result = spawnSync('/usr/bin/python3', [helperPath, 'not-a-profile'], {
|
||||
encoding: 'utf8',
|
||||
timeout: 5000,
|
||||
});
|
||||
expect({
|
||||
status: result.status,
|
||||
signal: result.signal,
|
||||
stdout: result.stdout,
|
||||
stderr: result.stderr,
|
||||
}).toEqual({
|
||||
status: 1,
|
||||
signal: null,
|
||||
stdout: 'result=refused\n',
|
||||
stderr: '',
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,71 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { spawnSync } = require('child_process');
|
||||
|
||||
const deployPath = name => path.join(__dirname, '..', 'scripts', 'deploy', name);
|
||||
const deploy = name => fs.readFileSync(deployPath(name), 'utf8');
|
||||
|
||||
describe('parallel Monero stagenet wallet candidate boundary', () => {
|
||||
const preflight = deploy('monero-stagenet-parallel-preflight.py');
|
||||
const unit = deploy('monerogue-stagenet-wallet-candidate.service');
|
||||
const runbook = deploy('README-monero-stagenet-parallel-candidate.md');
|
||||
|
||||
test('preflight is fixed, read-only, and does not inspect password-bearing sources', () => {
|
||||
expect(preflight).toContain('LEGACY_UNIT = "monero-wallet-rpc.service"');
|
||||
expect(preflight).toContain('LEGACY_RPC_PORT = 38083');
|
||||
expect(preflight).toContain('CANDIDATE_RPC_PORT = 38084');
|
||||
expect(preflight).toContain('VERIFY_RPC_PORT = 39084');
|
||||
expect(preflight).toContain('PR_SET_DUMPABLE');
|
||||
expect(preflight).toContain('O_NOFOLLOW');
|
||||
expect(preflight).toContain('details.st_nlink != 1');
|
||||
expect(preflight).not.toContain('/cmdline');
|
||||
expect(preflight).not.toContain('ExecStart');
|
||||
expect(preflight).not.toContain('wallet-password');
|
||||
expect(preflight).not.toMatch(/systemctl\([^\n]*(?:stop|start|restart|enable)/);
|
||||
expect(preflight).not.toMatch(/os\.(?:mkdir|unlink|remove|rename|chmod|chown|replace)\(/);
|
||||
expect(preflight).not.toContain('os.O_WRONLY');
|
||||
expect(preflight).not.toContain('mainnet');
|
||||
});
|
||||
|
||||
test('candidate unit is a manual inert service with no live-app dependency', () => {
|
||||
expect(unit).toContain('User=monerogue-wallet-candidate');
|
||||
expect(unit).toContain('Group=monerogue-wallet-candidate');
|
||||
expect(unit).toContain('SupplementaryGroups=');
|
||||
expect(unit).toContain('ConditionPathExists=/var/lib/monerogue-stagenet-wallet-candidate/READY');
|
||||
expect(unit).toContain('--config-file /var/lib/monerogue-stagenet-wallet-candidate/config/wallet-rpc.conf');
|
||||
expect(unit).toContain('--log-file /dev/null');
|
||||
expect(unit).toContain('Restart=no');
|
||||
expect(unit).toContain('LimitCORE=0');
|
||||
expect(unit).toContain('ProtectSystem=strict');
|
||||
expect(unit).not.toContain('[Install]');
|
||||
expect(unit).not.toContain('WantedBy=');
|
||||
expect(unit).not.toContain('monerogue.service');
|
||||
expect(unit).not.toContain('38083');
|
||||
expect(unit).not.toContain('mainnet');
|
||||
});
|
||||
|
||||
test('historical design record remains non-authorizing and fail-closed', () => {
|
||||
expect(runbook).toContain('STATUS: HISTORICAL NON-MUTATING DESIGN RECORD');
|
||||
expect(runbook).toContain('this document and its sample unit authorize nothing');
|
||||
expect(runbook).toContain('never alters the legacy artifacts');
|
||||
expect(runbook).toContain('renameat2(RENAME_NOREPLACE)');
|
||||
expect(runbook).toContain('write `READY` **last**');
|
||||
expect(runbook).toContain('production-deployable answer is **no**');
|
||||
expect(runbook).toContain('mnemonic/off-host recovery custody is resolved separately');
|
||||
});
|
||||
|
||||
test('disposable filesystem fixture passes independently', () => {
|
||||
const result = spawnSync(
|
||||
'/usr/bin/python3',
|
||||
[deployPath('monero-stagenet-parallel-fixture-test.py')],
|
||||
{ encoding: 'utf8', timeout: 15000 }
|
||||
);
|
||||
expect({ status: result.status, signal: result.signal, stderr: result.stderr }).toEqual({
|
||||
status: 0,
|
||||
signal: null,
|
||||
stderr: expect.stringContaining('OK')
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user