import { strict as assert } from 'node:assert' import { test } from 'node:test' import { Config, SETTING_KEYS, validateSettings } from '../index.js' /** * The load-time contract. * * `apply` never runs when Cordis rejects a plugin's `Config`, so these are the * only failures a plugin cannot report from inside itself — the profile simply * refuses to boot. The rest of the suite calls `apply` directly and would never * notice, which is exactly why this file exists separately. */ test('Config is a Standard Schema, which is what Cordis requires', () => { assert.ok(Config !== undefined, 'Config must be exported') assert.ok(Config['~standard'] !== undefined, 'Config must implement the Standard Schema interface') }) test('Config fills every field from an empty input, so apply can rely on defaults', () => { const filled = Config({}) for (const key of SETTING_KEYS) { assert.ok(key in filled, `${key} has no default and would be undefined in apply`) } assert.equal(filled.embeddingApiBase, 'http://127.0.0.1:11434') assert.equal(filled.embeddingDimensions, 768) assert.equal(filled.syncBudgetMs, 20_000) assert.deepEqual(filled.exclude, []) assert.deepEqual(filled.extensions, []) }) test('Config keeps an explicit value and fills the rest', () => { const filled = Config({ embeddingApiBase: 'http://10.50.1.2:11434', embeddingConcurrency: 8 }) assert.equal(filled.embeddingApiBase, 'http://10.50.1.2:11434') assert.equal(filled.embeddingConcurrency, 8) assert.equal(filled.embeddingBatchSize, 16) assert.equal(filled.embeddingTimeoutMs, 30_000) assert.equal(filled.embeddingKeepAlive, '5m') // The per-excerpt cap equals the chunk cap, so chunks arrive whole. assert.equal(filled.excerptLines, 80) assert.equal(filled.totalExcerptLines, 400) assert.equal(filled.maxChunkLines, 80) }) test('a wrongly typed value fails loudly at load rather than silently in apply', () => { assert.throws(() => Config({ embeddingDimensions: 'seven hundred' })) assert.throws(() => Config({ exclude: 'src/generated/' })) assert.throws(() => Config({ embeddingApiBase: { host: 'localhost' } })) }) test('every declared setting is a real schema key, so none is dead config', () => { const filled = Config({}) for (const key of SETTING_KEYS) { assert.ok(Object.hasOwn(filled, key), `${key} is listed in SETTING_KEYS but is not in the schema`) } // And the reverse: no schema field is missing from the documented list, or // `configure` would reject a setting the schema happily accepts. for (const key of Object.keys(filled)) { assert.ok(SETTING_KEYS.includes(key), `${key} is in the schema but not in SETTING_KEYS`) } }) /** The defaults plus an override, as the settings provider would resolve them. */ function settingsWith(patch) { return { ...Config({}), ...patch } } test('validateSettings accepts the defaults', () => { assert.doesNotThrow(() => validateSettings(Config({}))) }) test('validateSettings rejects an endpoint that is not a URL', () => { // A bare host:port parses successfully — `localhost:` is read as the scheme — // so it is the protocol check that actually catches the common mistake. assert.throws(() => validateSettings(settingsWith({ embeddingApiBase: 'localhost:11434' })), /must be http or https/) assert.throws(() => validateSettings(settingsWith({ embeddingApiBase: '' })), /not a URL/) assert.doesNotThrow(() => validateSettings(settingsWith({ embeddingApiBase: 'http://10.50.1.2:11434' }))) }) test('validateSettings rejects a non-http protocol', () => { assert.throws(() => validateSettings(settingsWith({ embeddingApiBase: 'file:///tmp/socket' })), /must be http or https/) }) test('validateSettings rejects values that would break the pipeline', () => { assert.throws(() => validateSettings(settingsWith({ embeddingDimensions: 0 })), /positive number/) assert.throws(() => validateSettings(settingsWith({ embeddingConcurrency: 0 })), /at least 1/) assert.throws(() => validateSettings(settingsWith({ excerptLines: 0 })), /at least 1/) assert.throws(() => validateSettings(settingsWith({ syncBudgetMs: -1 })), /cannot be negative/) }) test('validateSettings rejects cross-field contradictions the schema cannot see', () => { assert.throws(() => validateSettings(settingsWith({ defaultLimit: 50, maxLimit: 10 })), /cannot exceed maxLimit/) assert.throws(() => validateSettings(settingsWith({ chunkLines: 100, maxChunkLines: 80 })), /below chunkLines/) // An overlap at or above the cap means a split never advances. assert.throws(() => validateSettings(settingsWith({ chunkLines: 10, maxChunkLines: 10, overlapLines: 10 })), /never advances/) assert.throws(() => validateSettings(settingsWith({ minSimilarity: 1.5 })), /between 0 and 1/) })