• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

VolvoxLLC / volvox-bot / 22531409341

28 Feb 2026 11:32PM UTC coverage: 90.523% (+0.003%) from 90.52%
22531409341

Pull #154

github

web-flow
Merge c6e350266 into d66e0f9e2
Pull Request #154: docs: OpenAPI 3.0 spec with Mintlify documentation site

4300 of 5021 branches covered (85.64%)

Branch coverage included in aggregate %.

5 of 6 new or added lines in 3 files covered. (83.33%)

18 existing lines in 1 file now uncovered.

7344 of 7842 relevant lines covered (93.65%)

50.69 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

87.39
/src/api/utils/configValidation.js
1
/**
2
 * Shared config validation utilities.
3
 *
4
 * Centralises CONFIG_SCHEMA, validateValue, and validateSingleValue so that
5
 * both route handlers and util modules can import from a single source of
6
 * truth without creating an inverted dependency (utils → routes).
7
 */
8

9
/**
10
 * Schema definitions for writable config sections.
11
 * Used to validate types before persisting changes.
12
 */
13
export const CONFIG_SCHEMA = {
50✔
14
  ai: {
15
    type: 'object',
16
    properties: {
17
      enabled: { type: 'boolean' },
18
      systemPrompt: { type: 'string' },
19
      channels: { type: 'array' },
20
      historyLength: { type: 'number' },
21
      historyTTLDays: { type: 'number' },
22
      threadMode: {
23
        type: 'object',
24
        properties: {
25
          enabled: { type: 'boolean' },
26
          autoArchiveMinutes: { type: 'number' },
27
          reuseWindowMinutes: { type: 'number' },
28
        },
29
      },
30
    },
31
  },
32
  welcome: {
33
    type: 'object',
34
    properties: {
35
      enabled: { type: 'boolean' },
36
      channelId: { type: 'string', nullable: true },
37
      message: { type: 'string' },
38
      dynamic: {
39
        type: 'object',
40
        properties: {
41
          enabled: { type: 'boolean' },
42
          timezone: { type: 'string' },
43
          activityWindowMinutes: { type: 'number' },
44
          milestoneInterval: { type: 'number' },
45
          highlightChannels: { type: 'array' },
46
          excludeChannels: { type: 'array' },
47
        },
48
      },
49
      rulesChannel: { type: 'string', nullable: true },
50
      verifiedRole: { type: 'string', nullable: true },
51
      introChannel: { type: 'string', nullable: true },
52
      roleMenu: {
53
        type: 'object',
54
        properties: {
55
          enabled: { type: 'boolean' },
56
          options: { type: 'array', items: { type: 'object', required: ['label', 'roleId'] } },
57
        },
58
      },
59
      dmSequence: {
60
        type: 'object',
61
        properties: {
62
          enabled: { type: 'boolean' },
63
          steps: { type: 'array', items: { type: 'string' } },
64
        },
65
      },
66
    },
67
  },
68
  spam: {
69
    type: 'object',
70
    properties: {
71
      enabled: { type: 'boolean' },
72
    },
73
  },
74
  moderation: {
75
    type: 'object',
76
    properties: {
77
      enabled: { type: 'boolean' },
78
      alertChannelId: { type: 'string', nullable: true },
79
      autoDelete: { type: 'boolean' },
80
      dmNotifications: {
81
        type: 'object',
82
        properties: {
83
          warn: { type: 'boolean' },
84
          timeout: { type: 'boolean' },
85
          kick: { type: 'boolean' },
86
          ban: { type: 'boolean' },
87
        },
88
      },
89
      escalation: {
90
        type: 'object',
91
        properties: {
92
          enabled: { type: 'boolean' },
93
          thresholds: { type: 'array' },
94
        },
95
      },
96
      logging: {
97
        type: 'object',
98
        properties: {
99
          channels: {
100
            type: 'object',
101
            properties: {
102
              default: { type: 'string', nullable: true },
103
              warns: { type: 'string', nullable: true },
104
              bans: { type: 'string', nullable: true },
105
              kicks: { type: 'string', nullable: true },
106
              timeouts: { type: 'string', nullable: true },
107
              purges: { type: 'string', nullable: true },
108
              locks: { type: 'string', nullable: true },
109
            },
110
          },
111
        },
112
      },
113
    },
114
  },
115
  triage: {
116
    type: 'object',
117
    properties: {
118
      enabled: { type: 'boolean' },
119
      defaultInterval: { type: 'number' },
120
      maxBufferSize: { type: 'number' },
121
      triggerWords: { type: 'array' },
122
      moderationKeywords: { type: 'array' },
123
      classifyModel: { type: 'string' },
124
      classifyBudget: { type: 'number' },
125
      respondModel: { type: 'string' },
126
      respondBudget: { type: 'number' },
127
      thinkingTokens: { type: 'number' },
128
      classifyBaseUrl: { type: 'string', nullable: true },
129
      classifyApiKey: { type: 'string', nullable: true },
130
      respondBaseUrl: { type: 'string', nullable: true },
131
      respondApiKey: { type: 'string', nullable: true },
132
      streaming: { type: 'boolean' },
133
      tokenRecycleLimit: { type: 'number' },
134
      contextMessages: { type: 'number' },
135
      timeout: { type: 'number' },
136
      moderationResponse: { type: 'boolean' },
137
      channels: { type: 'array' },
138
      excludeChannels: { type: 'array' },
139
      debugFooter: { type: 'boolean' },
140
      debugFooterLevel: { type: 'string', nullable: true },
141
      moderationLogChannel: { type: 'string', nullable: true },
142
    },
143
  },
144
};
145

146
/**
147
 * Validate a value against a schema fragment and collect any validation errors.
148
 *
149
 * @param {*} value - The value to validate.
150
 * @param {Object} schema - Schema fragment describing the expected shape; may include `type` (boolean|string|number|array|object), `nullable`, and `properties` for object children.
151
 * @param {string} path - Dot-notation path used to prefix validation error messages.
152
 * @returns {string[]} Array of validation error messages; empty if the value is valid for the provided schema.
153
 */
154
export function validateValue(value, schema, path) {
155
  const errors = [];
116✔
156

157
  if (value === null) {
116✔
158
    if (!schema.nullable) {
10✔
159
      errors.push(`${path}: must not be null`);
4✔
160
    }
161
    return errors;
10✔
162
  }
163

164
  if (value === undefined) {
106!
165
    return errors;
×
166
  }
167

168
  switch (schema.type) {
106✔
169
    case 'boolean':
170
      if (typeof value !== 'boolean') {
37✔
171
        errors.push(`${path}: expected boolean, got ${typeof value}`);
8✔
172
      }
173
      break;
37✔
174
    case 'string':
175
      if (typeof value !== 'string') {
20✔
176
        errors.push(`${path}: expected string, got ${typeof value}`);
1✔
177
      }
178
      break;
20✔
179
    case 'number':
180
      if (typeof value !== 'number' || !Number.isFinite(value)) {
10✔
181
        errors.push(`${path}: expected finite number, got ${typeof value}`);
4✔
182
      }
183
      break;
10✔
184
    case 'array':
185
      if (!Array.isArray(value)) {
6✔
186
        errors.push(`${path}: expected array, got ${typeof value}`);
2✔
187
      } else if (schema.items) {
4✔
188
        for (let i = 0; i < value.length; i++) {
2✔
189
          const item = value[i];
3✔
190
          if (schema.items.type === 'string') {
3✔
191
            if (typeof item !== 'string') {
2!
192
              errors.push(`${path}[${i}]: expected string, got ${typeof item}`);
×
193
            }
194
          } else if (schema.items.type === 'object') {
1!
195
            if (typeof item !== 'object' || item === null || Array.isArray(item)) {
1!
NEW
196
              errors.push(
×
197
                `${path}[${i}]: expected object, got ${Array.isArray(item) ? 'array' : item === null ? 'null' : typeof item}`,
×
198
              );
199
            } else if (schema.items.required) {
1!
200
              for (const key of schema.items.required) {
1✔
201
                if (!(key in item)) {
2✔
202
                  errors.push(`${path}[${i}]: missing required key "${key}"`);
1✔
203
                }
204
              }
205
            }
206
          }
207
        }
208
      }
209
      break;
6✔
210
    case 'object':
211
      if (typeof value !== 'object' || Array.isArray(value)) {
33✔
212
        errors.push(
1✔
213
          `${path}: expected object, got ${Array.isArray(value) ? 'array' : typeof value}`,
1!
214
        );
215
      } else if (schema.properties) {
32!
216
        for (const [key, val] of Object.entries(value)) {
32✔
217
          if (Object.hasOwn(schema.properties, key)) {
46✔
218
            errors.push(...validateValue(val, schema.properties[key], `${path}.${key}`));
44✔
219
          } else {
220
            errors.push(`${path}.${key}: unknown config key`);
2✔
221
          }
222
        }
223
      }
224
      break;
33✔
225
  }
226

227
  return errors;
106✔
228
}
229

230
/**
231
 * Validate a single configuration path and its value against the writable config schema.
232
 *
233
 * @param {string} path - Dot-notation config path (e.g. "ai.enabled").
234
 * @param {*} value - The value to validate for the given path.
235
 * @returns {string[]} Array of validation error messages (empty if valid).
236
 */
237
export function validateSingleValue(path, value) {
238
  const segments = path.split('.');
46✔
239
  const section = segments[0];
46✔
240

241
  const schema = CONFIG_SCHEMA[section];
46✔
242
  if (!schema) return []; // unknown section — let SAFE_CONFIG_KEYS guard handle it
46✔
243

244
  // Walk the schema tree to find the leaf schema for this path
245
  let currentSchema = schema;
44✔
246
  for (let i = 1; i < segments.length; i++) {
44✔
247
    if (!currentSchema.properties || !Object.hasOwn(currentSchema.properties, segments[i])) {
51✔
248
      return [`Unknown config path: ${path}`];
4✔
249
    }
250
    currentSchema = currentSchema.properties[segments[i]];
47✔
251
  }
252

253
  return validateValue(value, currentSchema, path);
40✔
254
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc