mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 17:40:46 -04:00
leg
This commit is contained in:
@@ -73,6 +73,24 @@ function collectSchemaPathsMissingInputExamples(schema, value, pathLabel = '$',
|
||||
return Array.isArray(schema.examples) && schema.examples.length ? [] : [pathLabel];
|
||||
}
|
||||
|
||||
function collectEmptyStringPaths(value, pathLabel = '$') {
|
||||
/*
|
||||
Empty-string policy is intentionally tested by path because these four
|
||||
fields are exceptional for security or visible behavior, not omissions in
|
||||
the legacy-style default document. Walking the complete value also catches
|
||||
an accidentally blank field inside a pre-populated example collection.
|
||||
*/
|
||||
if (Array.isArray(value)) {
|
||||
return value.flatMap((item, index) => collectEmptyStringPaths(item, `${pathLabel}[${index}]`));
|
||||
}
|
||||
if (value && typeof value === 'object') {
|
||||
return Object.entries(value).flatMap(([key, childValue]) => (
|
||||
collectEmptyStringPaths(childValue, `${pathLabel}.${key}`)
|
||||
));
|
||||
}
|
||||
return value === '' ? [pathLabel] : [];
|
||||
}
|
||||
|
||||
test.after(() => {
|
||||
temporaryRoots.forEach((root) => fs.rmSync(root, { recursive: true, force: true }));
|
||||
});
|
||||
@@ -85,6 +103,25 @@ test('safe defaults form a complete valid configuration with integrations disabl
|
||||
assert.equal(defaultConfig.balanceBoard.enabled, false);
|
||||
});
|
||||
|
||||
test('legacy-style defaults populate every non-secret and inactive-content value', () => {
|
||||
/*
|
||||
Credentials must not masquerade as configured, and driver HTML would be
|
||||
immediately visible without an enable switch. Every other free-form value
|
||||
should match the populated template behavior operators had with YAML.
|
||||
*/
|
||||
assert.deepEqual(collectEmptyStringPaths(defaultConfig), [
|
||||
'$.homeAssistant.token',
|
||||
'$.ptzCamera.password',
|
||||
'$.discord.token',
|
||||
'$.driverAd.html',
|
||||
]);
|
||||
assert.ok(defaultConfig.interInstance.directoryUrls.length > 0);
|
||||
assert.ok(defaultConfig.homeAssistant.entities.length > 0);
|
||||
assert.ok(defaultConfig.homeAssistant.buttons.length > 0);
|
||||
assert.ok(defaultConfig.roomCameras.cameras.length > 0);
|
||||
assert.ok(defaultConfig.socials.links.length > 0);
|
||||
});
|
||||
|
||||
test('service definitions determine document order and write-only secret handling', () => {
|
||||
/*
|
||||
The generic browser form and backend persistence both consume this one
|
||||
@@ -174,7 +211,10 @@ test('generated feature flags use only each declared enabled switch', () => {
|
||||
|
||||
test('normalization fills missing legacy fields but strict validation rejects unknown fields', () => {
|
||||
const normalized = normalizeConfig({ media: { whepBaseUrl: 'http://localhost:8889/video' } });
|
||||
assert.deepEqual(normalized.media.additionalHosts, []);
|
||||
// Missing fields now receive the same populated template defaults as a new
|
||||
// installation; normalization must not silently revert this one collection
|
||||
// to the former empty-safe-default policy.
|
||||
assert.deepEqual(normalized.media.additionalHosts, ['rover.example.com', 'media-server.local']);
|
||||
assert.doesNotThrow(() => assertValidConfig(normalized));
|
||||
|
||||
const invalid = normalizeConfig({ media: { whepBaseUrl: 'http://localhost:8889/video', misspelledHost: 'x' } });
|
||||
|
||||
@@ -6,7 +6,9 @@ const { strictObject, string, boolean } = require('../../configuration/schemaHel
|
||||
module.exports = {
|
||||
key: 'barcodeGames',
|
||||
feature: true,
|
||||
defaultValue: { enabled: false, botName: 'Barcode Games', profileImageUrl: '' },
|
||||
// Disabled-by-default feature state is independent from its complete visual
|
||||
// identity, matching how the legacy YAML template represented this service.
|
||||
defaultValue: { enabled: false, botName: 'Barcode Games', profileImageUrl: 'https://example.com/barcode-games.png' },
|
||||
schema: strictObject({
|
||||
enabled: boolean({ description: 'Enables shared barcode-game voting, participation, scoring, and game-state publication.' }),
|
||||
botName: string({ description: 'Nickname used for barcode-game lifecycle messages posted into chat.', minLength: 1, maxLength: 80 }),
|
||||
|
||||
@@ -6,13 +6,26 @@ const { strictObject, string, boolean } = require('../../configuration/schemaHel
|
||||
module.exports = {
|
||||
key: 'discord',
|
||||
feature: true,
|
||||
// Channel and role IDs retain the fully populated legacy-template shape, but
|
||||
// the credential remains empty and the bot cannot start until explicitly enabled.
|
||||
defaultValue: {
|
||||
enabled: false,
|
||||
token: '',
|
||||
guildId: '',
|
||||
siteUrl: '',
|
||||
channels: { general: '', announcements: '', adminAlerts: '', replay: '', humanAlerts: '' },
|
||||
roles: { stalkerPing: '', announcementPing: '', adminPing: '', humanAlertPing: '' },
|
||||
guildId: '123456789012345678',
|
||||
siteUrl: 'https://rover.example.com',
|
||||
channels: {
|
||||
general: '123456789012345678',
|
||||
announcements: '123456789012345678',
|
||||
adminAlerts: '123456789012345678',
|
||||
replay: '123456789012345678',
|
||||
humanAlerts: '123456789012345678',
|
||||
},
|
||||
roles: {
|
||||
stalkerPing: '123456789012345678',
|
||||
announcementPing: '123456789012345678',
|
||||
adminPing: '123456789012345678',
|
||||
humanAlertPing: '123456789012345678',
|
||||
},
|
||||
},
|
||||
schema: strictObject({
|
||||
enabled: boolean({ description: 'Logs the Discord bot in and enables commands, chat bridges, replay delivery, and configured announcements after restart.' }),
|
||||
|
||||
@@ -11,14 +11,45 @@ module.exports = {
|
||||
// Retain the actual child definitions so generic configuration metadata can
|
||||
// discover their feature switches without repeating nested paths centrally.
|
||||
nestedDefinitions: [neato, lift],
|
||||
// Example entities and triggers are real initial document values, as they
|
||||
// were in the YAML template. Home Assistant stays inert until enabled and a
|
||||
// real secret is deliberately installed by the operator.
|
||||
defaultValue: {
|
||||
enabled: false,
|
||||
url: 'http://127.0.0.1:8123',
|
||||
token: '',
|
||||
[neato.key]: neato.defaultValue,
|
||||
[lift.key]: lift.defaultValue,
|
||||
entities: [],
|
||||
buttons: [],
|
||||
entities: [
|
||||
{ id: 'light.lab_main', name: 'Lab Lights' },
|
||||
{ id: 'switch.dock_power', name: 'Dock Power' },
|
||||
],
|
||||
buttons: [
|
||||
{
|
||||
entityId: 'sensor.basement_rover_buttons_action',
|
||||
stateEquals: 'on',
|
||||
cooldownMs: 15000,
|
||||
action: 'humanAlert',
|
||||
},
|
||||
{
|
||||
entityId: 'sensor.basement_rover_buttons_action',
|
||||
stateEquals: 'double',
|
||||
cooldownMs: 2000,
|
||||
action: 'modeTurns',
|
||||
},
|
||||
{
|
||||
entityId: 'sensor.basement_rover_buttons_action',
|
||||
stateEquals: 'hold',
|
||||
cooldownMs: 2000,
|
||||
action: 'modeAdmin',
|
||||
},
|
||||
{
|
||||
entityId: 'sensor.basement_rover_buttons_action',
|
||||
stateEquals: 'toggle',
|
||||
cooldownMs: 1000,
|
||||
action: 'lightsLockToggle',
|
||||
},
|
||||
],
|
||||
},
|
||||
schema: strictObject({
|
||||
enabled: boolean({ description: 'Connects to Home Assistant and enables configured room entities, physical-button triggers, Neato controls, and lift controls after restart.' }),
|
||||
|
||||
@@ -6,12 +6,19 @@ const { strictObject, string, boolean, integer, stringArray } = require('../../c
|
||||
module.exports = {
|
||||
key: 'interInstance',
|
||||
feature: true,
|
||||
// Optional behavior remains disabled, but a new configuration now starts
|
||||
// with the same complete, editable template that the former YAML supplied.
|
||||
defaultValue: {
|
||||
enabled: false,
|
||||
directoryUrls: [],
|
||||
directoryUrls: ['https://raw.githubusercontent.com/legop3/multi-roomba-rover-instance-directory/refs/heads/main/directory.json'],
|
||||
pollIntervalMs: 30000,
|
||||
requestTimeoutMs: 5000,
|
||||
profile: { publicUrl: '', name: 'MultiRover', description: '', color: '#38bdf8' },
|
||||
profile: {
|
||||
publicUrl: 'https://rover.example.com',
|
||||
name: 'Example Rover Server',
|
||||
description: 'A short public description of this rover server.',
|
||||
color: '#38bdf8',
|
||||
},
|
||||
},
|
||||
schema: strictObject({
|
||||
enabled: boolean({ description: 'Publishes this server\'s public instance information and polls the configured directories for peer servers.' }),
|
||||
|
||||
@@ -6,7 +6,15 @@ const { strictObject, string, boolean, integer } = require('../../configuration/
|
||||
module.exports = {
|
||||
key: 'lift',
|
||||
feature: true,
|
||||
defaultValue: { enabled: false, upSwitch: '', downSwitch: '', interlockMs: 2000, commandCooldownMs: 3000 },
|
||||
// These inert example entity IDs preserve the complete former YAML shape;
|
||||
// the explicit feature switch remains the only activation signal.
|
||||
defaultValue: {
|
||||
enabled: false,
|
||||
upSwitch: 'switch.lift_up',
|
||||
downSwitch: 'switch.lift_down',
|
||||
interlockMs: 2000,
|
||||
commandCooldownMs: 3000,
|
||||
},
|
||||
schema: strictObject({
|
||||
enabled: boolean({ description: 'Enables lift status and commands through the two configured Home Assistant switches after restart.' }),
|
||||
upSwitch: string({ description: 'Home Assistant switch entity that powers upward lift movement.', examples: ['switch.lift_up'], maxLength: 255 }),
|
||||
|
||||
@@ -5,7 +5,12 @@ const { strictObject, string, stringArray } = require('../../configuration/schem
|
||||
|
||||
module.exports = {
|
||||
key: 'media',
|
||||
defaultValue: { whepBaseUrl: 'http://127.0.0.1:8889/video', additionalHosts: [] },
|
||||
defaultValue: {
|
||||
// Signaling remains server-local because the internal `/video` proxy owns
|
||||
// browser access; only ICE transport addresses come from the legacy sample.
|
||||
whepBaseUrl: 'http://127.0.0.1:8889/video',
|
||||
additionalHosts: ['rover.example.com', 'media-server.local'],
|
||||
},
|
||||
schema: strictObject({
|
||||
whepBaseUrl: string({ title: 'WHEP base URL', description: 'Base HTTP URL used to build browser WHEP playback and WHIP audio-publishing endpoints.', format: 'uri', maxLength: 2048 }),
|
||||
additionalHosts: stringArray({
|
||||
|
||||
@@ -6,7 +6,9 @@ const { strictObject, string, boolean } = require('../../configuration/schemaHel
|
||||
module.exports = {
|
||||
key: 'neato',
|
||||
feature: true,
|
||||
defaultValue: { enabled: false, device: '' },
|
||||
// Keeping the example device in the saved template explains the required
|
||||
// ESPHome naming shape while `enabled: false` prevents accidental control.
|
||||
defaultValue: { enabled: false, device: 'neato_vacuum' },
|
||||
schema: strictObject({
|
||||
enabled: boolean({ description: 'Exposes Neato status and commands through the configured Home Assistant ESPHome device after restart.' }),
|
||||
device: string({ description: 'ESPHome device name used to derive the Neato entity IDs in Home Assistant; punctuation is normalized to underscores.', examples: ['neato_vacuum'], maxLength: 255 }),
|
||||
|
||||
@@ -5,6 +5,8 @@ const { strictObject, string, boolean, integer } = require('../../configuration/
|
||||
|
||||
module.exports = {
|
||||
key: 'overseerControl',
|
||||
// The integration is still opt-in; populated presentation values keep its
|
||||
// initial document complete without causing the model loop to start.
|
||||
defaultValue: {
|
||||
enabled: false,
|
||||
mode: 'autonomous',
|
||||
@@ -15,7 +17,7 @@ module.exports = {
|
||||
name: 'The Overseer',
|
||||
model: 'qwen2.5:7b-instruct',
|
||||
ollamaServer: 'http://127.0.0.1:11434',
|
||||
profileImageUrl: '',
|
||||
profileImageUrl: 'https://example.com/overseer.png',
|
||||
gateIntervalMs: 2000,
|
||||
},
|
||||
schema: strictObject({
|
||||
|
||||
@@ -6,13 +6,15 @@ const { strictObject, string, boolean, integer } = require('../../configuration/
|
||||
module.exports = {
|
||||
key: 'ptzCamera',
|
||||
feature: true,
|
||||
// Non-secret commissioning values mirror the legacy template. The password
|
||||
// remains empty and `enabled: false` prevents an accidental camera login.
|
||||
defaultValue: {
|
||||
enabled: false,
|
||||
name: 'PTZ Camera',
|
||||
color: '#38bdf8',
|
||||
host: '',
|
||||
host: '192.168.0.8',
|
||||
onvifPort: 8000,
|
||||
username: '',
|
||||
username: 'admin',
|
||||
password: '',
|
||||
profileToken: '003',
|
||||
turnDurationMs: 300000,
|
||||
|
||||
@@ -6,7 +6,27 @@ const { strictObject, string, boolean } = require('../../configuration/schemaHel
|
||||
module.exports = {
|
||||
key: 'roomCameras',
|
||||
feature: true,
|
||||
defaultValue: { enabled: false, cameras: [] },
|
||||
// The example catalog documents the complete repeated-item shape as actual
|
||||
// initial configuration while the feature switch prevents network requests.
|
||||
defaultValue: {
|
||||
enabled: false,
|
||||
cameras: [
|
||||
{
|
||||
id: 'lobby',
|
||||
name: 'Lobby Camera',
|
||||
description: 'Wide shot of the staging area.',
|
||||
url: 'http://192.168.0.50/snapshot.jpg',
|
||||
streamUrl: 'http://192.168.0.50/stream.mjpg',
|
||||
},
|
||||
{
|
||||
id: 'workshop',
|
||||
name: 'Workshop Bench',
|
||||
description: 'Shows the workbench and charging docks.',
|
||||
url: 'http://192.168.0.51/snapshot.jpg',
|
||||
streamUrl: 'http://192.168.0.51/stream.mjpg',
|
||||
},
|
||||
],
|
||||
},
|
||||
schema: strictObject({
|
||||
enabled: boolean({ description: 'Publishes the configured room-camera catalog and enables camera snapshots and streams after restart.' }),
|
||||
cameras: {
|
||||
|
||||
@@ -12,7 +12,27 @@ const timezone = {
|
||||
const socials = {
|
||||
key: 'socials',
|
||||
feature: true,
|
||||
defaultValue: { enabled: false, links: [] },
|
||||
// A complete starter list matches the former YAML template without exposing
|
||||
// it to users until the service-owned enabled switch is deliberately set.
|
||||
defaultValue: {
|
||||
enabled: false,
|
||||
links: [
|
||||
{
|
||||
id: 'discord',
|
||||
label: 'Discord',
|
||||
url: 'https://discord.gg/your-invite',
|
||||
icon: 'FaDiscord',
|
||||
color: '#5865F2',
|
||||
},
|
||||
{
|
||||
id: 'kofi',
|
||||
label: 'Ko-fi',
|
||||
url: 'https://ko-fi.com/your-handle',
|
||||
icon: 'FaCoffee',
|
||||
color: '#29ABE0',
|
||||
},
|
||||
],
|
||||
},
|
||||
schema: strictObject({
|
||||
enabled: boolean({ title: 'Enabled', description: 'Shows the configured social-link buttons in driver and inter-instance views.' }),
|
||||
links: {
|
||||
@@ -32,7 +52,9 @@ const socials = {
|
||||
|
||||
const driverAd = {
|
||||
key: 'driverAd',
|
||||
defaultValue: { title: '', html: '' },
|
||||
// The title is harmless presentation metadata, while the HTML stays empty so
|
||||
// a new installation never displays active sample content to drivers.
|
||||
defaultValue: { title: 'Advertisement', html: '' },
|
||||
schema: strictObject({
|
||||
title: string({ description: 'Heading displayed above the operator-provided content on the driver page; leave blank to use the card fallback.', examples: ['Advertisement'], maxLength: 120 }),
|
||||
html: string({ title: 'HTML', description: 'Trusted operator HTML shown to drivers.', examples: ['<a href="https://example.com" target="_blank" rel="noopener noreferrer"><img src="https://example.com/ad.png" alt="Advertisement"></a>'], maxLength: 100000 }),
|
||||
|
||||
Reference in New Issue
Block a user