mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 17:40:46 -04:00
168 lines
7.0 KiB
JavaScript
168 lines
7.0 KiB
JavaScript
// Operator Command Dispatcher Tests
|
|
// Purpose: Pins shared command permissions, access modes, and prefix parsing.
|
|
// Scope: Exercises the router with doubles; individual command behavior is covered by each command's own tests.
|
|
const test = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const { createCommandHandlers } = require('./index');
|
|
|
|
const MODES = { OPEN: 'open', TURNS: 'turns', ADMIN: 'admin', LOCKDOWN: 'lockdown' };
|
|
const ADMIN_DENIAL = /Only admins can run that command/;
|
|
const LOCKDOWN_DENIAL = /Lockdown mode: only lockdown admins/;
|
|
const FEATURE_DENIAL = /Admin mode: only admins can run feature commands/;
|
|
|
|
function createRouter({ mode = MODES.OPEN, featureEnabled = true } = {}) {
|
|
const rovers = new Map([['rover-1', {
|
|
id: 'rover-1',
|
|
ws: {},
|
|
meta: { name: 'Roomba One', horn: { enabled: true } },
|
|
batteryState: { percentDisplay: 50 },
|
|
}]]);
|
|
|
|
const { handleCommand } = createCommandHandlers({
|
|
logger: { warn: () => {}, info: () => {} },
|
|
io: { sockets: { sockets: new Map() } },
|
|
rovers,
|
|
roverManager: {},
|
|
getMode: () => mode,
|
|
MODES,
|
|
getNickname: (entry) => entry?.data?.nickname || '',
|
|
getActiveDrivers: () => ({}),
|
|
isFeatureEnabled: () => featureEnabled,
|
|
sanitizeMentions: (text) => String(text || ''),
|
|
homeAssistantService: { getLightPolicyState: () => ({}), setAllControllableEntitiesState: () => Promise.resolve() },
|
|
// Green mode is admin-only at the dispatcher, so this double only needs to
|
|
// prove that an authorized command reaches the small standalone service.
|
|
greenModeService: { setEnabled: (enabled) => Promise.resolve(Boolean(enabled)) },
|
|
liftService: null,
|
|
neatoService: null,
|
|
listVerifiedUsers: () => [],
|
|
listDeterredUsers: () => [],
|
|
listMutedUsers: () => [],
|
|
listUsersForAdmin: () => [],
|
|
listUsersWithPermission: () => [],
|
|
listRegisteredPermissions: () => [{
|
|
key: 'audio.personalAdjustment',
|
|
commandName: 'audio-adjustment',
|
|
label: 'Personal audio adjustment',
|
|
description: 'Allows personal volume adjustments.',
|
|
}],
|
|
setUserPermission: () => null,
|
|
getGlobalObjective: () => null,
|
|
getAdminReason: () => null,
|
|
config: { commands: { prefix: 'rs' } },
|
|
transportHandlers: {
|
|
status: async (request) => request.reply({ content: '[status handler]' }),
|
|
bridge: async (request) => request.reply({ content: '[bridge handler]' }),
|
|
},
|
|
});
|
|
|
|
return async function run(text, actor) {
|
|
const replies = [];
|
|
await handleCommand({
|
|
content: text,
|
|
transport: 'web-chat',
|
|
actor,
|
|
reply: async (payload) => {
|
|
replies.push(typeof payload === 'string' ? payload : payload?.content);
|
|
},
|
|
});
|
|
return replies.join('\n');
|
|
};
|
|
}
|
|
|
|
const nonAdmin = { id: 's1', userId: 'u-alice', label: 'alice', isAdmin: false, isLockdownAdmin: false };
|
|
const admin = { id: 's1', userId: 'u-alice', label: 'alice', isAdmin: true, isLockdownAdmin: false };
|
|
|
|
test('admin-only commands stay admin-only for a non-admin', async () => {
|
|
const run = createRouter();
|
|
for (const command of ['rs lock rover-1', 'rs unlock rover-1', 'rs mode open', 'rs green on', 'rs kick alice', 'rs permissions list', 'rs ha lock Fan speed']) {
|
|
assert.match(await run(command, nonAdmin), ADMIN_DENIAL, `${command} must stay admin-only`);
|
|
}
|
|
});
|
|
|
|
// Activity locks obey the same stronger restriction as other moderation actions.
|
|
test('activity lock commands require lockdown admin during lockdown', async () => {
|
|
const run = createRouter({ mode: MODES.LOCKDOWN });
|
|
assert.match(await run('rs ha unlock Fan speed', admin), LOCKDOWN_DENIAL);
|
|
});
|
|
|
|
test('commands that police themselves still reach their handler as a non-admin', async () => {
|
|
const run = createRouter();
|
|
// These reply with their own role-specific message, so the dispatcher must not
|
|
// short-circuit them with the generic admin denial.
|
|
for (const command of ['rs goal', 'rs reason', 'rs verify list', 'rs deter list']) {
|
|
assert.doesNotMatch(await run(command, nonAdmin), ADMIN_DENIAL, `${command} enforces its own permission`);
|
|
}
|
|
});
|
|
|
|
test('system commands remain reachable by anyone', async () => {
|
|
const run = createRouter();
|
|
assert.match(await run('rs status', nonAdmin), /\[status handler\]/);
|
|
assert.match(await run('rs', nonAdmin), /\[status handler\]/);
|
|
assert.match(await run('rs help', nonAdmin), /Rover Bot Commands/);
|
|
});
|
|
|
|
test('admin mode restricts access-mode feature commands', async () => {
|
|
const run = createRouter({ mode: MODES.ADMIN });
|
|
assert.match(await run('rs lights on', nonAdmin), FEATURE_DENIAL);
|
|
});
|
|
|
|
test('lockdown still suspends the pre-existing moderation-sensitive commands', async () => {
|
|
const run = createRouter({ mode: MODES.LOCKDOWN });
|
|
for (const command of ['rs lock rover-1', 'rs mode open', 'rs lights on', 'rs green on', 'rs goal', 'rs kick alice']) {
|
|
assert.match(await run(command, admin), LOCKDOWN_DENIAL, `${command} should stay lockdown-gated`);
|
|
}
|
|
});
|
|
|
|
test('admins can toggle green mode through the shared command path', async () => {
|
|
const run = createRouter();
|
|
assert.match(await run('rs green on', admin), /Green mode enabled/);
|
|
assert.match(await run('rs green off', admin), /Green mode disabled/);
|
|
});
|
|
|
|
test('green mode rejects unknown actions with focused usage guidance', async () => {
|
|
const run = createRouter();
|
|
assert.match(await run('rs green maybe', admin), /rs green on.*rs green off/);
|
|
});
|
|
|
|
test('status and help survive lockdown', async () => {
|
|
const run = createRouter({ mode: MODES.LOCKDOWN });
|
|
assert.match(await run('rs status', nonAdmin), /\[status handler\]/);
|
|
assert.match(await run('rs help', nonAdmin), /Rover Bot Commands/);
|
|
});
|
|
|
|
test('a disabled required feature is reported before any permission check', async () => {
|
|
const run = createRouter({ featureEnabled: false });
|
|
assert.match(await run('rs lights on', nonAdmin), /Home Assistant feature is disabled/);
|
|
});
|
|
|
|
test('green mode remains available without optional Home Assistant features', async () => {
|
|
const run = createRouter({ featureEnabled: false });
|
|
assert.match(await run('rs green on', admin), /Green mode enabled/);
|
|
});
|
|
|
|
test('ordinary words that merely start with the prefix are not commands', async () => {
|
|
const run = createRouter();
|
|
assert.equal(await run('rsvp', nonAdmin), '');
|
|
assert.equal(await run('rspecial delivery', nonAdmin), '');
|
|
assert.equal(await run('hello there', nonAdmin), '');
|
|
});
|
|
|
|
test('an unknown command is not treated as public', async () => {
|
|
const run = createRouter();
|
|
// Unknown actions carry no registry entry, so they must fall through to the
|
|
// same admin denial they did before the permission refactor.
|
|
assert.match(await run('rs notacommand', nonAdmin), ADMIN_DENIAL);
|
|
assert.match(await run('rs notacommand', admin), /Rover Bot Commands/);
|
|
});
|
|
|
|
test('bot actors are ignored entirely', async () => {
|
|
const run = createRouter();
|
|
assert.equal(await run('rs status', { ...nonAdmin, bot: true }), '');
|
|
});
|
|
|
|
test('command matching is case insensitive', async () => {
|
|
const run = createRouter();
|
|
assert.match(await run('Rs Status', nonAdmin), /\[status handler\]/);
|
|
});
|