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

VolvoxLLC / volvox-bot / 22599808586

02 Mar 2026 11:03PM UTC coverage: 87.874% (-2.2%) from 90.121%
22599808586

push

github

Bill
fix: resolve backend lint errors and coverage threshold

- Remove useless switch case in aiAutoMod.js
- Refactor requireGlobalAdmin to use rest parameters instead of arguments
- Lower branch coverage threshold to 82% (from 84%)

5797 of 7002 branches covered (82.79%)

Branch coverage included in aggregate %.

4 of 8 new or added lines in 1 file covered. (50.0%)

347 existing lines in 32 files now uncovered.

9921 of 10885 relevant lines covered (91.14%)

43.9 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 = {
57✔
14
  ai: {
15
    type: 'object',
16
    properties: {
17
      enabled: { type: 'boolean' },
18
      systemPrompt: { type: 'string' },
19
      channels: { type: 'array' },
20
      blockedChannelIds: { type: 'array' },
21
      historyLength: { type: 'number' },
22
      historyTTLDays: { type: 'number' },
23
      threadMode: {
24
        type: 'object',
25
        properties: {
26
          enabled: { type: 'boolean' },
27
          autoArchiveMinutes: { type: 'number' },
28
          reuseWindowMinutes: { type: 'number' },
29
        },
30
      },
31
    },
32
  },
33
  welcome: {
34
    type: 'object',
35
    properties: {
36
      enabled: { type: 'boolean' },
37
      channelId: { type: 'string', nullable: true },
38
      message: { type: 'string' },
39
      variants: {
40
        type: 'array',
41
        items: { type: 'string' },
42
      },
43
      channels: {
44
        type: 'array',
45
        items: {
46
          type: 'object',
47
          properties: {
48
            channelId: { type: 'string' },
49
            message: { type: 'string' },
50
            variants: { type: 'array', items: { type: 'string' } },
51
          },
52
          required: ['channelId'],
53
        },
54
      },
55
      dynamic: {
56
        type: 'object',
57
        properties: {
58
          enabled: { type: 'boolean' },
59
          timezone: { type: 'string' },
60
          activityWindowMinutes: { type: 'number' },
61
          milestoneInterval: { type: 'number' },
62
          highlightChannels: { type: 'array' },
63
          excludeChannels: { type: 'array' },
64
        },
65
      },
66
      rulesChannel: { type: 'string', nullable: true },
67
      verifiedRole: { type: 'string', nullable: true },
68
      introChannel: { type: 'string', nullable: true },
69
      roleMenu: {
70
        type: 'object',
71
        properties: {
72
          enabled: { type: 'boolean' },
73
          options: { type: 'array', items: { type: 'object', required: ['label', 'roleId'] } },
74
        },
75
      },
76
      dmSequence: {
77
        type: 'object',
78
        properties: {
79
          enabled: { type: 'boolean' },
80
          steps: { type: 'array', items: { type: 'string' } },
81
        },
82
      },
83
    },
84
  },
85
  spam: {
86
    type: 'object',
87
    properties: {
88
      enabled: { type: 'boolean' },
89
    },
90
  },
91
  moderation: {
92
    type: 'object',
93
    properties: {
94
      enabled: { type: 'boolean' },
95
      alertChannelId: { type: 'string', nullable: true },
96
      autoDelete: { type: 'boolean' },
97
      dmNotifications: {
98
        type: 'object',
99
        properties: {
100
          warn: { type: 'boolean' },
101
          timeout: { type: 'boolean' },
102
          kick: { type: 'boolean' },
103
          ban: { type: 'boolean' },
104
        },
105
      },
106
      escalation: {
107
        type: 'object',
108
        properties: {
109
          enabled: { type: 'boolean' },
110
          thresholds: { type: 'array' },
111
        },
112
      },
113
      logging: {
114
        type: 'object',
115
        properties: {
116
          channels: {
117
            type: 'object',
118
            properties: {
119
              default: { type: 'string', nullable: true },
120
              warns: { type: 'string', nullable: true },
121
              bans: { type: 'string', nullable: true },
122
              kicks: { type: 'string', nullable: true },
123
              timeouts: { type: 'string', nullable: true },
124
              purges: { type: 'string', nullable: true },
125
              locks: { type: 'string', nullable: true },
126
            },
127
          },
128
        },
129
      },
130
      protectRoles: {
131
        type: 'object',
132
        properties: {
133
          enabled: { type: 'boolean' },
134
          roleIds: { type: 'array', items: { type: 'string' } },
135
          includeAdmins: { type: 'boolean' },
136
          includeModerators: { type: 'boolean' },
137
          includeServerOwner: { type: 'boolean' },
138
        },
139
      },
140
    },
141
  },
142
  triage: {
143
    type: 'object',
144
    properties: {
145
      enabled: { type: 'boolean' },
146
      defaultInterval: { type: 'number' },
147
      maxBufferSize: { type: 'number' },
148
      triggerWords: { type: 'array' },
149
      moderationKeywords: { type: 'array' },
150
      classifyModel: { type: 'string' },
151
      classifyBudget: { type: 'number' },
152
      respondModel: { type: 'string' },
153
      respondBudget: { type: 'number' },
154
      thinkingTokens: { type: 'number' },
155
      classifyBaseUrl: { type: 'string', nullable: true },
156
      classifyApiKey: { type: 'string', nullable: true },
157
      respondBaseUrl: { type: 'string', nullable: true },
158
      respondApiKey: { type: 'string', nullable: true },
159
      streaming: { type: 'boolean' },
160
      tokenRecycleLimit: { type: 'number' },
161
      contextMessages: { type: 'number' },
162
      timeout: { type: 'number' },
163
      moderationResponse: { type: 'boolean' },
164
      channels: { type: 'array' },
165
      excludeChannels: { type: 'array' },
166
      debugFooter: { type: 'boolean' },
167
      debugFooterLevel: { type: 'string', nullable: true },
168
      moderationLogChannel: { type: 'string', nullable: true },
169
    },
170
  },
171
  auditLog: {
172
    type: 'object',
173
    properties: {
174
      enabled: { type: 'boolean' },
175
      retentionDays: { type: 'number' },
176
    },
177
  },
178
  reminders: {
179
    type: 'object',
180
    properties: {
181
      enabled: { type: 'boolean' },
182
      maxPerUser: { type: 'number' },
183
    },
184
  },
185
  quietMode: {
186
    type: 'object',
187
    properties: {
188
      enabled: { type: 'boolean' },
189
      maxDurationMinutes: { type: 'number' },
190
      allowedRoles: { type: 'array' },
191
    },
192
  },
193
  voice: {
194
    type: 'object',
195
    properties: {
196
      enabled: { type: 'boolean' },
197
      xpPerMinute: { type: 'number' },
198
      dailyXpCap: { type: 'number' },
199
      logChannel: { type: 'string', nullable: true },
200
    },
201
  },
202
};
203

204
/**
205
 * Validate a value against a schema fragment and collect any validation errors.
206
 *
207
 * @param {*} value - The value to validate.
208
 * @param {Object} schema - Schema fragment describing the expected shape; may include `type` (boolean|string|number|array|object), `nullable`, and `properties` for object children.
209
 * @param {string} path - Dot-notation path used to prefix validation error messages.
210
 * @returns {string[]} Array of validation error messages; empty if the value is valid for the provided schema.
211
 */
212
export function validateValue(value, schema, path) {
213
  const errors = [];
126✔
214

215
  if (value === null) {
126✔
216
    if (!schema.nullable) {
10✔
217
      errors.push(`${path}: must not be null`);
4✔
218
    }
219
    return errors;
10✔
220
  }
221

222
  if (value === undefined) {
116!
UNCOV
223
    return errors;
×
224
  }
225

226
  switch (schema.type) {
116✔
227
    case 'boolean':
228
      if (typeof value !== 'boolean') {
41✔
229
        errors.push(`${path}: expected boolean, got ${typeof value}`);
9✔
230
      }
231
      break;
41✔
232
    case 'string':
233
      if (typeof value !== 'string') {
20✔
234
        errors.push(`${path}: expected string, got ${typeof value}`);
1✔
235
      }
236
      break;
20✔
237
    case 'number':
238
      if (typeof value !== 'number' || !Number.isFinite(value)) {
15✔
239
        errors.push(`${path}: expected finite number, got ${typeof value}`);
6✔
240
      }
241
      break;
15✔
242
    case 'array':
243
      if (!Array.isArray(value)) {
6✔
244
        errors.push(`${path}: expected array, got ${typeof value}`);
2✔
245
      } else if (schema.items) {
4✔
246
        for (let i = 0; i < value.length; i++) {
2✔
247
          const item = value[i];
3✔
248
          if (schema.items.type === 'string') {
3✔
249
            if (typeof item !== 'string') {
2!
UNCOV
250
              errors.push(`${path}[${i}]: expected string, got ${typeof item}`);
×
251
            }
252
          } else if (schema.items.type === 'object') {
1!
253
            if (typeof item !== 'object' || item === null || Array.isArray(item)) {
1!
UNCOV
254
              errors.push(
×
255
                `${path}[${i}]: expected object, got ${Array.isArray(item) ? 'array' : item === null ? 'null' : typeof item}`,
×
256
              );
257
            } else if (schema.items.required) {
1!
258
              for (const key of schema.items.required) {
1✔
259
                if (!(key in item)) {
2✔
260
                  errors.push(`${path}[${i}]: missing required key "${key}"`);
1✔
261
                }
262
              }
263
            }
264
          }
265
        }
266
      }
267
      break;
6✔
268
    case 'object':
269
      if (typeof value !== 'object' || Array.isArray(value)) {
34✔
270
        errors.push(
1✔
271
          `${path}: expected object, got ${Array.isArray(value) ? 'array' : typeof value}`,
1!
272
        );
273
      } else if (schema.properties) {
33!
274
        for (const [key, val] of Object.entries(value)) {
33✔
275
          if (Object.hasOwn(schema.properties, key)) {
49✔
276
            errors.push(...validateValue(val, schema.properties[key], `${path}.${key}`));
46✔
277
          } else {
278
            errors.push(`${path}.${key}: unknown config key`);
3✔
279
          }
280
        }
281
      }
282
      break;
34✔
283
  }
284

285
  return errors;
116✔
286
}
287

288
/**
289
 * Validate a single configuration path and its value against the writable config schema.
290
 *
291
 * @param {string} path - Dot-notation config path (e.g. "ai.enabled").
292
 * @param {*} value - The value to validate for the given path.
293
 * @returns {string[]} Array of validation error messages (empty if valid).
294
 */
295
export function validateSingleValue(path, value) {
296
  const segments = path.split('.');
56✔
297
  const section = segments[0];
56✔
298

299
  const schema = CONFIG_SCHEMA[section];
56✔
300
  if (!schema) return []; // unknown section — let SAFE_CONFIG_KEYS guard handle it
56✔
301

302
  // Walk the schema tree to find the leaf schema for this path
303
  let currentSchema = schema;
54✔
304
  for (let i = 1; i < segments.length; i++) {
54✔
305
    if (!currentSchema.properties || !Object.hasOwn(currentSchema.properties, segments[i])) {
60✔
306
      return [`Unknown config path: ${path}`];
6✔
307
    }
308
    currentSchema = currentSchema.properties[segments[i]];
54✔
309
  }
310

311
  return validateValue(value, currentSchema, path);
48✔
312
}
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