This commit is contained in:
legop3
2026-09-14 13:04:41 -04:00
parent 6edb6f6dd0
commit 0266bd9568
13 changed files with 175 additions and 21 deletions
+41 -1
View File
@@ -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 }),