Files
dsh-slopo/test/config.test.js
T
dsh-slopo agent 3373e0b4f6 Test the plugin against the real harness seams and the real CLI
The integration tests load the real plugin module against a thin mock Cordis
context and drive the tool through a real shell into the real slopo binary. The
mock implements only the seams the plugin touches, so a wrong assumption about a
dsh service fails a test instead of producing a plugin that silently does
nothing at runtime.

Two real bugs were caught this way and are now pinned:

  * renderSlopoConfig would write the embedding API key to disk if it were
    handed one. It cannot now.
  * --config-version was placed before the subcommand, which slopo rejects with
    exit code 2. It is declared on the command, not the callback.

The end-to-end test is opt-in (DSH_SLOPO_E2E=1) because it needs a reachable
embedding backend; it indexes a real repository, embeds it, and asserts a real
duplicate cluster comes back.
2026-09-22 06:11:17 +01:00

108 lines
4.4 KiB
JavaScript

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/)
})