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

VolvoxLLC / volvox-bot / 22536363373

01 Mar 2026 04:59AM UTC coverage: 90.104% (-0.2%) from 90.276%
22536363373

push

github

BillChirico
fix: resolve CI failures on main (lint, coverage, secret scanning)

- Migrate biome.json schema to 2.4.4 and fix formatting/lint violations
- Replace isNaN with Number.isNaN in db.js
- Remove unused getConfig import in ticket tests
- Fix import ordering in scheduler and reload tests
- Fix noArrayIndexKey lint in config-editor.tsx
- Add welcome command and scheduler tick tests to meet 85% branch threshold
- Allowlist historical false positive commit in .gitleaks.toml

4581 of 5384 branches covered (85.09%)

Branch coverage included in aggregate %.

4 of 4 new or added lines in 2 files covered. (100.0%)

106 existing lines in 14 files now uncovered.

7856 of 8419 relevant lines covered (93.31%)

49.06 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 = {
52✔
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
  auditLog: {
145
    type: 'object',
146
    properties: {
147
      enabled: { type: 'boolean' },
148
      retentionDays: { type: 'number' },
149
    },
150
  },
151
  reminders: {
152
    type: 'object',
153
    properties: {
154
      enabled: { type: 'boolean' },
155
      maxPerUser: { type: 'number' },
156
    },
157
  },
158
};
159

160
/**
161
 * Validate a value against a schema fragment and collect any validation errors.
162
 *
163
 * @param {*} value - The value to validate.
164
 * @param {Object} schema - Schema fragment describing the expected shape; may include `type` (boolean|string|number|array|object), `nullable`, and `properties` for object children.
165
 * @param {string} path - Dot-notation path used to prefix validation error messages.
166
 * @returns {string[]} Array of validation error messages; empty if the value is valid for the provided schema.
167
 */
168
export function validateValue(value, schema, path) {
169
  const errors = [];
126✔
170

171
  if (value === null) {
126✔
172
    if (!schema.nullable) {
10✔
173
      errors.push(`${path}: must not be null`);
4✔
174
    }
175
    return errors;
10✔
176
  }
177

178
  if (value === undefined) {
116!
UNCOV
179
    return errors;
×
180
  }
181

182
  switch (schema.type) {
116✔
183
    case 'boolean':
184
      if (typeof value !== 'boolean') {
41✔
185
        errors.push(`${path}: expected boolean, got ${typeof value}`);
9✔
186
      }
187
      break;
41✔
188
    case 'string':
189
      if (typeof value !== 'string') {
20✔
190
        errors.push(`${path}: expected string, got ${typeof value}`);
1✔
191
      }
192
      break;
20✔
193
    case 'number':
194
      if (typeof value !== 'number' || !Number.isFinite(value)) {
15✔
195
        errors.push(`${path}: expected finite number, got ${typeof value}`);
6✔
196
      }
197
      break;
15✔
198
    case 'array':
199
      if (!Array.isArray(value)) {
6✔
200
        errors.push(`${path}: expected array, got ${typeof value}`);
2✔
201
      } else if (schema.items) {
4✔
202
        for (let i = 0; i < value.length; i++) {
2✔
203
          const item = value[i];
3✔
204
          if (schema.items.type === 'string') {
3✔
205
            if (typeof item !== 'string') {
2!
UNCOV
206
              errors.push(`${path}[${i}]: expected string, got ${typeof item}`);
×
207
            }
208
          } else if (schema.items.type === 'object') {
1!
209
            if (typeof item !== 'object' || item === null || Array.isArray(item)) {
1!
UNCOV
210
              errors.push(
×
211
                `${path}[${i}]: expected object, got ${Array.isArray(item) ? 'array' : item === null ? 'null' : typeof item}`,
×
212
              );
213
            } else if (schema.items.required) {
1!
214
              for (const key of schema.items.required) {
1✔
215
                if (!(key in item)) {
2✔
216
                  errors.push(`${path}[${i}]: missing required key "${key}"`);
1✔
217
                }
218
              }
219
            }
220
          }
221
        }
222
      }
223
      break;
6✔
224
    case 'object':
225
      if (typeof value !== 'object' || Array.isArray(value)) {
34✔
226
        errors.push(
1✔
227
          `${path}: expected object, got ${Array.isArray(value) ? 'array' : typeof value}`,
1!
228
        );
229
      } else if (schema.properties) {
33!
230
        for (const [key, val] of Object.entries(value)) {
33✔
231
          if (Object.hasOwn(schema.properties, key)) {
49✔
232
            errors.push(...validateValue(val, schema.properties[key], `${path}.${key}`));
46✔
233
          } else {
234
            errors.push(`${path}.${key}: unknown config key`);
3✔
235
          }
236
        }
237
      }
238
      break;
34✔
239
  }
240

241
  return errors;
116✔
242
}
243

244
/**
245
 * Validate a single configuration path and its value against the writable config schema.
246
 *
247
 * @param {string} path - Dot-notation config path (e.g. "ai.enabled").
248
 * @param {*} value - The value to validate for the given path.
249
 * @returns {string[]} Array of validation error messages (empty if valid).
250
 */
251
export function validateSingleValue(path, value) {
252
  const segments = path.split('.');
54✔
253
  const section = segments[0];
54✔
254

255
  const schema = CONFIG_SCHEMA[section];
54✔
256
  if (!schema) return []; // unknown section — let SAFE_CONFIG_KEYS guard handle it
54✔
257

258
  // Walk the schema tree to find the leaf schema for this path
259
  let currentSchema = schema;
52✔
260
  for (let i = 1; i < segments.length; i++) {
52✔
261
    if (!currentSchema.properties || !Object.hasOwn(currentSchema.properties, segments[i])) {
58✔
262
      return [`Unknown config path: ${path}`];
4✔
263
    }
264
    currentSchema = currentSchema.properties[segments[i]];
54✔
265
  }
266

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