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.
This commit is contained in:
1 parent
31991143e4
commit
3373e0b4f6
4 files changed
+610
-1
No files matched your search
@@ -0,0 +1,107 @@
|
||||
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/)
|
||||
})
|
||||
Reference in new issue
Block a user