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

VolvoxLLC / volvox-bot / 22531190565

28 Feb 2026 11:18PM UTC coverage: 90.19% (-0.3%) from 90.52%
22531190565

Pull #153

github

web-flow
Merge bd015e0cc into d66e0f9e2
Pull Request #153: feat: add /remind command with natural language time parsing

4409 of 5171 branches covered (85.26%)

Branch coverage included in aggregate %.

262 of 305 new or added lines in 7 files covered. (85.9%)

1 existing line in 1 file now uncovered.

7552 of 8091 relevant lines covered (93.34%)

49.21 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 = {
49✔
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
  reminders: {
145
    type: 'object',
146
    properties: {
147
      enabled: { type: 'boolean' },
148
      maxPerUser: { type: 'number' },
149
    },
150
  },
151
};
152

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

164
  if (value === null) {
116✔
165
    if (!schema.nullable) {
10✔
166
      errors.push(`${path}: must not be null`);
4✔
167
    }
168
    return errors;
10✔
169
  }
170

171
  if (value === undefined) {
106!
172
    return errors;
×
173
  }
174

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

234
  return errors;
106✔
235
}
236

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

248
  const schema = CONFIG_SCHEMA[section];
46✔
249
  if (!schema) return []; // unknown section — let SAFE_CONFIG_KEYS guard handle it
46✔
250

251
  // Walk the schema tree to find the leaf schema for this path
252
  let currentSchema = schema;
44✔
253
  for (let i = 1; i < segments.length; i++) {
44✔
254
    if (!currentSchema.properties || !Object.hasOwn(currentSchema.properties, segments[i])) {
51✔
255
      return [`Unknown config path: ${path}`];
4✔
256
    }
257
    currentSchema = currentSchema.properties[segments[i]];
47✔
258
  }
259

260
  return validateValue(value, currentSchema, path);
40✔
261
}
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