diff --git a/lib/providers/base.mjs b/lib/providers/base.mjs index 749b6a1..c4bacfd 100644 --- a/lib/providers/base.mjs +++ b/lib/providers/base.mjs @@ -24,6 +24,7 @@ * @property {boolean} requiresTTY * @property {boolean} concurrentSpawnSafe * @property {number} maxConcurrent + * @property {number} [maxSpawnTimeMs] - optional integer milliseconds, default 600000 */ /** @@ -110,13 +111,18 @@ export function validateProvider(p) { } if (!p.hints || typeof p.hints !== 'object') { - errors.push('hints must be an object with { requiresTTY, concurrentSpawnSafe, maxConcurrent }'); + errors.push('hints must be an object with { requiresTTY, concurrentSpawnSafe, maxConcurrent, maxSpawnTimeMs }'); } else { if (typeof p.hints.requiresTTY !== 'boolean') errors.push('hints.requiresTTY must be a boolean'); if (typeof p.hints.concurrentSpawnSafe !== 'boolean') errors.push('hints.concurrentSpawnSafe must be a boolean'); if (typeof p.hints.maxConcurrent !== 'number' || !Number.isInteger(p.hints.maxConcurrent) || p.hints.maxConcurrent < 0) { errors.push('hints.maxConcurrent must be a non-negative integer'); } + if (p.hints.maxSpawnTimeMs !== undefined) { + if (typeof p.hints.maxSpawnTimeMs !== 'number' || !Number.isInteger(p.hints.maxSpawnTimeMs) || p.hints.maxSpawnTimeMs <= 0) { + errors.push('hints.maxSpawnTimeMs must be a positive integer (milliseconds) or omitted'); + } + } } return { valid: errors.length === 0, errors }; diff --git a/test-features.mjs b/test-features.mjs index 2e8325c..556fd17 100644 --- a/test-features.mjs +++ b/test-features.mjs @@ -521,6 +521,42 @@ describe('Provider contract validation', () => { assert.ok(r.errors.some(e => e.includes('maxConcurrent'))); }); + it('validateProvider rejects negative maxSpawnTimeMs', () => { + const r = validateProvider(makeProvider({ hints: { requiresTTY: false, concurrentSpawnSafe: true, maxConcurrent: 4, maxSpawnTimeMs: -1 } })); + assert.equal(r.valid, false); + assert.ok(r.errors.some(e => e.includes('maxSpawnTimeMs'))); + }); + + it('validateProvider rejects zero maxSpawnTimeMs', () => { + const r = validateProvider(makeProvider({ hints: { requiresTTY: false, concurrentSpawnSafe: true, maxConcurrent: 4, maxSpawnTimeMs: 0 } })); + assert.equal(r.valid, false); + assert.ok(r.errors.some(e => e.includes('maxSpawnTimeMs'))); + }); + + it('validateProvider rejects non-integer maxSpawnTimeMs (e.g., 100.5)', () => { + const r = validateProvider(makeProvider({ hints: { requiresTTY: false, concurrentSpawnSafe: true, maxConcurrent: 4, maxSpawnTimeMs: 100.5 } })); + assert.equal(r.valid, false); + assert.ok(r.errors.some(e => e.includes('maxSpawnTimeMs'))); + }); + + it('validateProvider rejects non-number maxSpawnTimeMs (e.g., \'600\')', () => { + const r = validateProvider(makeProvider({ hints: { requiresTTY: false, concurrentSpawnSafe: true, maxConcurrent: 4, maxSpawnTimeMs: '600' } })); + assert.equal(r.valid, false); + assert.ok(r.errors.some(e => e.includes('maxSpawnTimeMs'))); + }); + + it('validateProvider accepts omitted maxSpawnTimeMs (optional field)', () => { + const r = validateProvider(makeProvider({ hints: { requiresTTY: false, concurrentSpawnSafe: true, maxConcurrent: 4 } })); + assert.equal(r.valid, true); + assert.deepEqual(r.errors, []); + }); + + it('validateProvider accepts positive integer maxSpawnTimeMs (e.g., 60000)', () => { + const r = validateProvider(makeProvider({ hints: { requiresTTY: false, concurrentSpawnSafe: true, maxConcurrent: 4, maxSpawnTimeMs: 60000 } })); + assert.equal(r.valid, true); + assert.deepEqual(r.errors, []); + }); + it('rejects non-object input', () => { const r = validateProvider(null); assert.equal(r.valid, false);