import assert from 'node:assert/strict' import { test } from 'node:test' import { SLOPO_CONFIG_KEYS, missingRequired, normalizeConfig, pruneConfig, renderSlopoConfig, } from '../lib/config.js' test('pruneConfig keeps only known keys and drops empties', () => { const pruned = pruneConfig({ source_dir: '/src', embedding_model: '', embedding_dimensions: 0, source_dir_exclude: [], unknown_key: 'nope', embedding_params: {}, }) assert.deepEqual(Object.keys(pruned), ['source_dir', 'embedding_dimensions']) assert.equal(pruned.source_dir, '/src') // 0 is a real value, not an empty one. assert.equal(pruned.embedding_dimensions, 0) }) test('pruneConfig silently refuses a key slopo would hard-error on', () => { const pruned = pruneConfig({ source_dir: '/src', totally_unknown: 'x' }) assert.equal(Object.hasOwn(pruned, 'totally_unknown'), false) for (const key of Object.keys(pruned)) assert.ok(SLOPO_CONFIG_KEYS.includes(key)) }) test('renderSlopoConfig emits key: value with a space, which slopo lints for', () => { const text = renderSlopoConfig({ source_dir: '/a b/c', embedding_dimensions: 768 }) assert.match(text, /^source_dir: "\/a b\/c"$/m) assert.match(text, /^embedding_dimensions: 768$/m) for (const line of text.split('\n')) { if (line === '' || line.startsWith('#') || line.startsWith(' ')) continue assert.doesNotMatch(line, /^[A-Za-z_][A-Za-z0-9_]*:\S/, `no-space-after-colon: ${line}`) } }) test('renderSlopoConfig always quotes strings so a colon or dash cannot change meaning', () => { const text = renderSlopoConfig({ embedding_model: 'ollama/model:latest', db_file: '/tmp/-weird#path/x.db', }) assert.match(text, /^embedding_model: "ollama\/model:latest"$/m) assert.match(text, /^db_file: "\/tmp\/-weird#path\/x\.db"$/m) }) test('renderSlopoConfig escapes a quote rather than emitting broken YAML', () => { const text = renderSlopoConfig({ source_dir: '/a"b' }) assert.match(text, /^source_dir: "\/a\\"b"$/m) }) test('renderSlopoConfig renders lists and nested maps', () => { const text = renderSlopoConfig({ source_dir_exclude: ['**/test/**', '*.min.js'], embedding_params: { api_base: 'http://127.0.0.1:11434' }, }) assert.match(text, /^source_dir_exclude:\n {2}- "\*\*\/test\/\*\*"\n {2}- "\*\.min\.js"$/m) assert.match(text, /^embedding_params:\n {2}api_base: "http:\/\/127\.0\.0\.1:11434"$/m) }) test('renderSlopoConfig never writes the embedding API key', () => { const text = renderSlopoConfig({ source_dir: '/src', embedding_api_key: 'sk-secret' }) assert.doesNotMatch(text, /sk-secret/) assert.doesNotMatch(text, /embedding_api_key/) }) test('renderSlopoConfig refuses to silently write a non-finite number', () => { assert.throws(() => renderSlopoConfig({ embedding_dimensions: Number.NaN }), TypeError) }) test('missingRequired reports exactly the three keys slopo refuses to start without', () => { assert.deepEqual(missingRequired({}), ['source_dir', 'embedding_model', 'embedding_dimensions']) assert.deepEqual(missingRequired({ source_dir: '/src', embedding_model: 'm', embedding_dimensions: 768 }), []) // A dimension of 0 counts as unset, not as configured. assert.deepEqual(missingRequired({ source_dir: '/s', embedding_model: 'm', embedding_dimensions: 0 }), [ 'embedding_dimensions', ]) }) test('normalizeConfig coerces numeric strings from a settings form', () => { const { config, problems } = normalizeConfig({ embedding_dimensions: '768', embedding_batch_size: '50' }) assert.deepEqual(problems, []) assert.equal(config.embedding_dimensions, 768) assert.equal(config.embedding_batch_size, 50) }) test('normalizeConfig rejects a non-numeric value instead of writing a corrupt config', () => { const { config, problems } = normalizeConfig({ embedding_dimensions: 'lots' }) assert.equal(Object.hasOwn(config, 'embedding_dimensions'), false) assert.equal(problems.length, 1) assert.match(problems[0], /embedding_dimensions must be a number/) }) test('normalizeConfig keeps thresholds fractional but rejects fractional counts', () => { const ok = normalizeConfig({ review_similarity_threshold: 0.92 }) assert.equal(ok.config.review_similarity_threshold, 0.92) assert.deepEqual(ok.problems, []) const bad = normalizeConfig({ embedding_batch_size: 12.5 }) assert.equal(Object.hasOwn(bad.config, 'embedding_batch_size'), false) assert.match(bad.problems[0], /whole number/) })