Files
forward_server/tests/world_voxels.test.ts
T
2508932142 c82f15e8b6 feat: relay world snapshot and voxel payloads
Allow world.* methods through the relay, validate their payloads, and
track per-session world revisions in the session store.
2026-10-23 10:44:42 +08:00

236 lines
15 KiB
TypeScript

import assert from 'node:assert/strict';
import {randomUUID} from 'node:crypto';
import {once} from 'node:events';
import {createServer} from 'node:http';
import test from 'node:test';
import {Server} from 'socket.io';
import {io, type Socket as Client} from 'socket.io-client';
import type {ForwardAck, NodeRelayRequest, RelayClaims, RelayMethod, WorldPayloads, WorldVoxelChunk} from '../types.ts';
import {install_relay_routes} from '../runtime/relay_routes.ts';
import {create_session_store} from '../runtime/session_store.ts';
import {DEFAULT_LIMITS} from '../utils/relay_config.ts';
import {RelayError, check_permissions, success, validate_node_ack, validate_request} from '../utils/relay_validation.ts';
import {encode_token} from '../utils/token_utils.ts';
const input = (): WorldPayloads['world.chunk'] => ({x: -12, z: 7, world_generation: randomUUID(), watch_id: randomUUID()});
const blocks = (index = 0, position = 4095) => {
const data = Buffer.alloc(8192);
data.writeUInt16LE(index, position * 2);
return data.toString('base64');
};
const chunk = (payload = input()): WorldVoxelChunk => ({
x: payload.x, z: payload.z, revision: randomUUID(), world_generation: payload.world_generation,
min_y: -64, height: 384,
palette: [{name: 'minecraft:air', color: 0, properties: {}, occluding: false},
{name: 'minecraft:stone', color: 0x777777, properties: {}, occluding: true},
{name: 'minecraft:oak_stairs', color: 0xb88754, properties: {facing: 'east', half: 'bottom', waterlogged: true}, occluding: false}],
sections: [{y: 0, blocks: blocks(1, 17)}, {y: 1, blocks: blocks(2, 2048)}],
});
const request = (payload: unknown) => validate_request({request_id: randomUUID(), method: 'world.chunk', payload}, DEFAULT_LIMITS);
const ack = (data: unknown, payload: unknown) => validate_node_ack('world.chunk', success(data), 3, DEFAULT_LIMITS, payload);
const rejected = (action: () => unknown) => assert.throws(action,
(error: unknown) => error instanceof RelayError && error.code === 'INVALID_REQUEST');
test('world.chunk requests require exact coordinates, generation and watch using existing permissions', () => {
const payload = input();
request(payload);
request({...payload, x: -1_875_000, z: 1_875_000});
request({...payload, x: 1_875_000, z: -1_875_000});
check_permissions(['bot.snapshot']);
rejected(() => check_permissions(['world.chunk']));
for (const key of Object.keys(payload)) {
const missing = {...payload} as Record<string, unknown>;
delete missing[key];
rejected(() => request(missing));
}
for (const patch of [{x: -1_875_001}, {z: 1_875_001}, {x: 0.5}, {z: '7'}, {x: NaN}, {z: Infinity},
{world_generation: 'bad'}, {watch_id: null}, {watch_id: 'bad'}, {world_generation: undefined},
{revision: randomUUID()}, {bot_id: 3}, {context_generation: randomUUID()}]) {
rejected(() => request({...payload, ...patch}));
}
for (const value of [null, [] as unknown[], 'chunk']) rejected(() => request(value));
});
test('world.chunk accepts empty sparse sections and preserves buried blocks and state in two real sections', () => {
const payload = input(), data = chunk(payload);
ack(data, payload);
assert.equal(Buffer.from(data.sections[0]!.blocks, 'base64').readUInt16LE(17 * 2), 1);
assert.equal(Buffer.from(data.sections[1]!.blocks, 'base64').readUInt16LE(2048 * 2), 2);
assert.equal(Buffer.from(data.sections[0]!.blocks, 'base64').readUInt16LE(4095 * 2), 0);
assert.equal(data.sections[0]!.blocks.length, 10_924);
ack({...data, sections: []}, payload);
ack({...data, min_y: -4096, height: 1, sections: [{y: 0, blocks: blocks()}]}, payload);
ack({...data, min_y: 4096, height: 4096, sections: [{y: 255, blocks: blocks()}]}, payload);
// The wire contract does not require zero padding beyond a partial section's height.
ack({...data, height: 17, sections: [{y: 1, blocks: blocks(1)}]}, payload);
ack({...data, world_generation: data.world_generation.toUpperCase()}, payload);
});
test('world.chunk ACK metadata is exact and matches request coordinates and world generation', () => {
const payload = input(), data = chunk(payload);
for (const key of Object.keys(data)) {
const missing = {...data} as Record<string, unknown>;
delete missing[key];
rejected(() => ack(missing, payload));
}
for (const patch of [{x: payload.x + 1}, {z: payload.z - 1}, {x: 1_875_001}, {z: 0.5},
{world_generation: randomUUID()}, {world_generation: 'bad'}, {revision: 'bad'},
{min_y: -4097}, {min_y: 4097}, {min_y: 0.5}, {height: 0}, {height: 4097}, {height: 1.5},
{watch_id: payload.watch_id}, {stream_id: randomUUID()}, {context_generation: randomUUID()}, {extra: true}]) {
rejected(() => ack({...data, ...patch}, payload));
}
for (const value of [null, [], {}, undefined]) rejected(() => ack(value, payload));
for (const value of [undefined, {}, {...payload, x: payload.x + 1}, {...payload, world_generation: randomUUID()}]) {
rejected(() => ack(data, value));
}
validate_node_ack('world.chunk', {status: 'error', data: {code: 'RESYNC_REQUIRED', message: 'Stale watch', retryable: true}}, 3, DEFAULT_LIMITS, payload);
});
test('world.chunk palette entries require bounded colors, names, occlusion and exact fields', () => {
const payload = input(), data = chunk(payload), block = data.palette[1]!;
const accept = (palette: unknown) => ack({...data, palette, sections: []}, payload);
accept([{...block, name: 'a', color: 0}]);
accept([{...block, name: 'x'.repeat(128), color: 0xffffff}]);
for (const palette of [[], null, {}, Array.from({length: 8193}, () => block)]) rejected(() => accept(palette));
for (const key of Object.keys(block)) {
const missing = {...block} as Record<string, unknown>;
delete missing[key];
rejected(() => accept([missing]));
}
for (const patch of [{name: ''}, {name: 'x'.repeat(129)}, {color: -1}, {color: 0x1000000}, {color: 0.5},
{occluding: 1}, {occluding: 'true'}, {extra: true}, {shape: 'cube'}, {properties: [] as unknown[]}, {properties: null}]) {
rejected(() => accept([{...block, ...patch}]));
}
});
test('world.chunk properties accept canonical state keys and bounded primitives without a semantic whitelist', () => {
const payload = input(), data = chunk(payload);
const accept = (properties: unknown) => ack({...data, palette: [{...data.palette[0], properties}], sections: []}, payload);
accept({custom_block_state: 'unknown_but_valid', rotation: 65_536, min: -65_536, solid: true, empty: '', zero: 0});
accept(Object.fromEntries(Array.from({length: 32}, (_, i) => [`property_${i}`, i % 2 === 0 ? 'x'.repeat(64) : false])));
accept({['k'.repeat(32)]: true});
for (const properties of [
Object.fromEntries(Array.from({length: 33}, (_, i) => [`p${i}`, true])),
{'': true}, {'Facing': 'east'}, {'minecraft:axis': 'x'}, {'a-b': true}, {'state\n': true}, {['k'.repeat(33)]: true},
{key: 'x'.repeat(65)}, {key: 65_537}, {key: -65_537}, {key: 0.5}, {key: null}, {key: [] as unknown[]}, {key: {}},
{key: undefined}, {key: NaN}, {key: Infinity},
]) rejected(() => accept(properties));
const getter = Object.defineProperty({}, 'state', {enumerable: true, get() {throw new Error('must not execute');}});
rejected(() => accept(getter));
rejected(() => accept(Object.defineProperty({}, 'state', {value: true})));
rejected(() => accept({[Symbol('state')]: true}));
});
test('world.chunk sections are sparse, unique, ascending and bounded by height', () => {
const payload = input(), data = chunk(payload), section = data.sections[0]!;
ack({...data, sections: [{...section, y: 3}, {...section, y: 23}]}, payload);
for (const sections of [null, {}, [section, section], [{...section, y: 1}, section], [{...section, y: -1}],
[{...section, y: 24}], [{...section, y: 0.5}], [{...section, y: '0'}], [{blocks: section.blocks}],
[{y: 0}], [{...section, extra: true}], Array.from({length: 257}, (_, y) => ({...section, y}))]) {
rejected(() => ack({...data, sections}, payload));
}
rejected(() => ack({...data, height: 16, sections: [{...section, y: 1}]}, payload));
rejected(() => ack({...data, sections: new Array(1)}, payload));
});
test('world.chunk section blocks enforce canonical base64, exact UInt16LE byte count and all palette indices', () => {
const payload = input(), data = chunk(payload), encoded = blocks();
const accept = (value: unknown) => ack({...data, sections: [{y: 0, blocks: value}]}, payload);
const noncanonical = `${encoded.slice(0, -2)}B=`;
assert.equal(Buffer.from(noncanonical, 'base64').toString('base64'), encoded);
for (const value of ['', encoded.slice(0, -1), `${encoded}\n`, 'A'.repeat(10_924), Buffer.alloc(8191).toString('base64'),
Buffer.alloc(8193).toString('base64'), noncanonical, `_${encoded.slice(1)}`, Buffer.alloc(8192), new Uint16Array(4096)]) {
rejected(() => accept(value));
}
for (const position of [0, 17, 2048, 4095]) {
accept(blocks(data.palette.length - 1, position));
rejected(() => accept(blocks(data.palette.length, position)));
rejected(() => accept(blocks(65_535, position)));
}
});
test('world.chunk maximum palette fits dedicated world complexity budget and preserves 8 MiB byte limits', () => {
const payload = input(), data = chunk(payload);
const properties = Object.fromEntries(Array.from({length: 32}, (_, index) => [`p${index}`, true]));
const maximum: WorldVoxelChunk = {...data, height: 4096,
palette: Array.from({length: 8192}, (_, index) => ({name: `block_${index}`, color: index, properties, occluding: true})),
sections: Array.from({length: 256}, (_, y) => ({y, blocks: blocks(8191)}))};
ack(maximum, payload);
assert.ok(Buffer.byteLength(JSON.stringify(success(maximum))) < 8 * 1024 * 1024);
const size = Buffer.byteLength(JSON.stringify(success(data)));
validate_node_ack('world.chunk', success(data), 3, {...DEFAULT_LIMITS, max_payload_bytes: size}, payload);
rejected(() => validate_node_ack('world.chunk', success(data), 3, {...DEFAULT_LIMITS, max_payload_bytes: size - 1}, payload));
const oversized = {...maximum, palette: maximum.palette.map(block => ({...block,
properties: Object.fromEntries(Object.keys(properties).map(key => [key, 'x'.repeat(64)]))}))};
rejected(() => ack(oversized, payload));
rejected(() => validate_node_ack('bot.snapshot', success(maximum), 3, DEFAULT_LIMITS));
});
test('world.chunk uses private correlated ACKs, snapshot permission on both roles and fresh validated reads', async t => {
const key = 'world-voxel-relay-test-secret';
const scope = {forward_id: 1, forward_session_id: randomUUID(), node_session_id: randomUUID(), node_id: 2, bot_id: 3};
const http = createServer(), server = new Server(http), namespace = server.of('/relay');
const store = create_session_store({forward_id: 1, key, limits: DEFAULT_LIMITS, on_status() {}});
const dispose = install_relay_routes(namespace, store), clients: Client[] = [];
t.after(async () => {store.close(); dispose(); for (const client of clients) client.close(); await server.close();});
http.listen(0, '127.0.0.1');
await once(http, 'listening');
store.open({...scope, lease_expires_at: Date.now() + 60_000});
const send = (socket: Client, event: string, value: unknown): Promise<ForwardAck<unknown>> => socket.timeout(3000).emitWithAck(event, value);
const connect = async (role: 'web' | 'node', permissions: RelayMethod[] = ['bot.snapshot']) => {
const socket = io(`http://127.0.0.1:${(http.address() as {port: number}).port}/relay`,
{transports: ['websocket'], forceNew: true, reconnection: false});
clients.push(socket);
await new Promise<void>(resolve => socket.once('connect', resolve));
const now = Date.now(), grant_id = randomUUID(), expires_at = now + 50_000;
if (role === 'web') store.authorize({forward_session_id: scope.forward_session_id, grant_id, user_id: 4, permissions, expires_at});
const claims: RelayClaims = {...scope, v: 1, role, permissions, issued_at: now, join_before: now + 10_000,
expires_at, jti: randomUUID(), ...(role === 'web' ? {grant_id, user_id: 4} : {})};
assert.equal((await send(socket, 'join_room', {token: encode_token(key, claims)})).status, 'success');
if (role === 'node') assert.equal((await send(socket, 'forward_node_ready', {forward_session_id: scope.forward_session_id})).status, 'success');
return socket;
};
const node = await connect('node'), web = await connect('web'), observer = await connect('web');
const limited = await connect('web', ['bot.logs']);
const payload = input(), command = {request_id: randomUUID(), method: 'world.chunk', payload};
const data = chunk(payload);
let response: unknown = success(data), calls = 0;
const leaked: string[] = [];
observer.onAny(event => leaked.push(event));
node.on('relay_request', (value: NodeRelayRequest, reply: (result: unknown) => void) => {
calls++;
assert.equal(value.method, 'world.chunk');
assert.deepEqual(value.payload, payload);
assert.equal(value.forward_session_id, scope.forward_session_id);
assert.notEqual(value.correlation_id, value.request_id);
assert.ok(value.deadline_at > Date.now());
reply(response);
});
const error = (value: ForwardAck<unknown>, expected: string) => {
assert.equal(value.status, 'error');
if (value.status === 'error') assert.equal(value.data.code, expected);
};
store.set_control_available(false);
assert.deepEqual(await send(web, 'relay_request', command), response);
response = success({...data, revision: randomUUID(), sections: []});
assert.deepEqual(await send(web, 'relay_request', command), response);
assert.equal(calls, 2);
for (const patch of [{x: data.x + 1}, {z: data.z + 1}, {world_generation: randomUUID()}, {sections: [{y: 0, blocks: blocks(3)}]}]) {
response = success({...data, ...patch});
error(await send(web, 'relay_request', command), 'INVALID_REQUEST');
}
response = {status: 'error', data: {code: 'RESYNC_REQUIRED', message: 'Stale watch', retryable: true}};
assert.deepEqual(await send(web, 'relay_request', command), response);
response = success(data);
assert.deepEqual(await send(web, 'relay_request', command), response);
const before = calls;
error(await send(limited, 'relay_request', command), 'FORBIDDEN');
error(await send(node, 'relay_request', command), 'FORBIDDEN');
assert.equal(calls, before);
assert.deepEqual(leaked, []);
store.set_control_available(true);
await connect('node', ['bot.logs']);
error(await send(web, 'relay_request', command), 'FORBIDDEN');
assert.equal(calls, before);
});