Test the scope defaults, the backlog gate and the database reader

The gate tests use a canned shell runner, and two of them build a real
slopo-shaped SQLite file to prove the warm-database hole is closed: the summary
line says zero new units while the database says 8,607 unembedded, and the gate
must believe the database.

Those fixtures insert in one transaction. Row-by-row they were 8,607 individual
fsyncs, which took two minutes and timed the suite out - worth knowing before
writing another one.
This commit is contained in:
dsh-slopo agent committed 2026-09-22 13:23:49 +01:00
1 parent e149b6a977
commit 22be1e819a
3 files changed
+334 -3

No files matched your search

+186 -1
View File
@@ -35,6 +35,8 @@ const FULL_CONFIG = {
stateDir: '',
embeddingRequestDelay: 0,
timeoutMs: 600_000,
useDefaultExcludes: true,
maxIndexUnits: 5000,
}
/** A shell executor that really runs the command. */
@@ -76,7 +78,7 @@ const shell = {
* `jobs` is deliberately absent, which forces the tool down its foreground path
* so the assertions can see a result rather than a job handle.
*/
function harness(overrides = {}, { withSettings = true, withJobs = false, withShell = true, sandboxMode, sandboxPolicyResolver } = {}) {
function harness(overrides = {}, { withSettings = true, withJobs = false, withShell = true, sandboxMode, sandboxPolicyResolver, shellRun } = {}) {
const config = { ...FULL_CONFIG, ...overrides }
// A per-harness shell, so a test can vary sandboxMode and every resolved
// request is recorded for assertion.
@@ -87,6 +89,8 @@ function harness(overrides = {}, { withSettings = true, withJobs = false, withSh
state.shellRequests.push(request)
return { workdir: process.cwd(), timeoutMs: 600_000, stdoutMaxBytes: 10_000_000, ...request }
},
// A canned runner lets a test drive the index gate without a real slopo.
...(shellRun === undefined ? {} : { run: shellRun }),
}
const state = {
tools: new Map(),
@@ -533,3 +537,184 @@ test('an action refused before it runs is lossless JSON', async () => {
assertLossless('review (state outside sandbox)', value)
assert.equal(value.errorCode, 'STATE_OUTSIDE_SANDBOX')
})
/** A shell runner returning canned slopo output, recording every command. */
function cannedShell(ran, { indexOutput, actionOutput = 'cluster 1:\n * /x/y.py:1-5', exitCode = 0 } = {}) {
return async (spec) => {
ran.push(spec.command)
// The command is shell-quoted, so a bare \sindex\s cannot match `'index'`.
const isIndex = /\bindex\b/.test(spec.command)
return {
exitCode: isIndex || exitCode === 0 ? 0 : exitCode,
aborted: false,
timedOut: false,
timeoutMs: spec.timeoutMs,
stdout: { text: isIndex ? indexOutput : actionOutput, truncated: false },
stderr: { text: '', truncated: false },
}
}
}
test('a scope over the budget is refused BEFORE anything is embedded', async () => {
// The regression: the plugin indexed the whole repository — node_modules and
// all, 113,768 units — and began an embed that would have run for about eight
// hours. Slopo's own >1000 guard cannot catch this, because it measures the
// pre-existing backlog and a fresh database has none.
const ran = []
const { state } = harness(
{ embeddingModel: 'ollama/example:latest', embeddingDimensions: 768 },
{ shellRun: cannedShell(ran, { indexOutput: 'Indexed 113768 code units from 1300 files (0 unchanged, 0 removed).' }) },
)
const value = await state.tools.get('slopo').execute({ action: 'review', foreground: true }, execContext('/tmp'))
assert.equal(value.ok, false)
assert.equal(value.errorCode, 'SCOPE_TOO_LARGE')
assert.match(value.message, /113,768 code units/)
assert.match(value.message, /still need embedding/)
assert.match(value.remedy, /sourceDirExclude/)
assert.equal(value.details.counted.units, 113_768)
assertLossless('scope refusal', value)
// The point of the gate: the expensive step never started.
assert.equal(ran.length, 1, `only the index step may run, ran: ${ran.join(' | ')}`)
assert.match(ran[0], /\bindex\b/)
assert.doesNotMatch(ran[0], /agent-review/)
})
test('a scope under the budget indexes and then runs the action', async () => {
const ran = []
const { state } = harness(
{ embeddingModel: 'ollama/example:latest', embeddingDimensions: 768 },
{ shellRun: cannedShell(ran, { indexOutput: 'Indexed 1519 code units from 165 files (0 unchanged, 0 removed).' }) },
)
const value = await state.tools.get('slopo').execute({ action: 'review', foreground: true }, execContext('/tmp'))
assert.equal(value.ok, true)
assert.match(value.report, /cluster 1/)
assert.equal(ran.length, 2)
assert.match(ran[0], /\bindex\b/)
assert.match(ran[1], /agent-review/)
})
test('raising the budget lets the same scope through', async () => {
const ran = []
const { state } = harness(
{ embeddingModel: 'ollama/example:latest', embeddingDimensions: 768, maxIndexUnits: 0 },
{ shellRun: cannedShell(ran, { indexOutput: 'Indexed 113768 code units from 1300 files.' }) },
)
const value = await state.tools.get('slopo').execute({ action: 'review', foreground: true }, execContext('/tmp'))
assert.equal(value.ok, true)
assert.equal(ran.length, 2)
})
test('the config slopo is handed excludes the defaults, and a user list is added', async () => {
const ran = []
const { state } = harness(
{
embeddingModel: 'ollama/example:latest',
embeddingDimensions: 768,
sourceDirExclude: ['ui/**', 'src/controllers/**'],
},
{ shellRun: cannedShell(ran, { indexOutput: 'Indexed 1519 code units from 165 files.' }) },
)
const value = await state.tools.get('slopo').execute({ action: 'review', foreground: true }, execContext('/tmp'))
const { readFileSync } = await import('node:fs')
const written = readFileSync(value.configPath, 'utf8')
for (const pattern of ['**/node_modules/**', 'ui/**', 'src/controllers/**']) {
assert.ok(written.includes(pattern), `the config slopo reads must exclude ${pattern}`)
}
})
test('slopo never sees a directory it should not, even when it is asked twice', async () => {
// Idempotence matters because indexing now runs on every action.
const ran = []
const { state } = harness(
{ embeddingModel: 'ollama/example:latest', embeddingDimensions: 768 },
{ shellRun: cannedShell(ran, { indexOutput: 'Indexed 1519 code units from 165 files.' }) },
)
const tool = state.tools.get('slopo')
await tool.execute({ action: 'prepare', foreground: true }, execContext('/tmp'))
const afterPrepare = ran.length
assert.match(ran[0], /\bindex\b/)
assert.match(ran[1], /\bembed\b/)
assert.equal(afterPrepare, 2)
})
test('status reports the excludes and the budget, so the levers are visible', async () => {
const { state } = harness({
embeddingModel: 'ollama/example:latest',
embeddingDimensions: 768,
sourceDirExclude: ['ui/**'],
})
const value = await state.tools.get('slopo').execute({ action: 'status' }, execContext('/tmp'))
assert.ok(value.details.sourceDirExclude.includes('**/node_modules/**'))
assert.ok(value.details.sourceDirExclude.includes('ui/**'))
assert.equal(value.details.useDefaultExcludes, true)
assert.equal(value.details.maxIndexUnits, 5000)
assertLossless('status', value)
})
test('the gate reads the DATABASE, so a warm index cannot hide a full backlog', async () => {
// The hole this closes: after a refusal the database exists with the whole
// scope unembedded, so the next run's `index` reports zero new units and a
// summary-line gate would wave the hours-long embed straight through.
const { mkdtempSync } = await import('node:fs')
const { tmpdir } = await import('node:os')
const { join } = await import('node:path')
const { DatabaseSync } = await import('node:sqlite')
const stateDir = mkdtempSync(join(tmpdir(), 'dsh-slopo-warm-'))
const db = new DatabaseSync(join(stateDir, 'slopo.db'))
for (const table of ['files', 'code_units', 'embeddings']) db.exec(`create table ${table} (id integer primary key)`)
const fill = (table, count) => {
const insert = db.prepare(`insert into ${table} (id) values (?)`)
for (let index = 0; index < count; index += 1) insert.run(index)
}
db.exec('begin')
fill('files', 562)
fill('code_units', 8607)
db.exec('commit')
db.close()
const ran = []
const { state } = harness(
{ embeddingModel: 'ollama/example:latest', embeddingDimensions: 768, stateDir },
{ shellRun: cannedShell(ran, { indexOutput: 'Indexed 0 code units from 0 files (562 unchanged, 0 removed).' }) },
)
const value = await state.tools.get('slopo').execute({ action: 'prepare', foreground: true }, execContext('/tmp'))
assert.equal(value.errorCode, 'SCOPE_TOO_LARGE', 'the summary line said zero, the database said 8,607')
assert.equal(value.details.counted.units, 8607)
assert.equal(value.details.counted.embedded, 0)
assert.equal(ran.length, 1, `only the index step may run, ran: ${ran.join(' | ')}`)
assertLossless('warm-database refusal', value)
})
test('a fully embedded index has no backlog and is never refused', async () => {
const { mkdtempSync } = await import('node:fs')
const { tmpdir } = await import('node:os')
const { join } = await import('node:path')
const { DatabaseSync } = await import('node:sqlite')
const stateDir = mkdtempSync(join(tmpdir(), 'dsh-slopo-done-'))
const db = new DatabaseSync(join(stateDir, 'slopo.db'))
for (const table of ['files', 'code_units', 'embeddings']) db.exec(`create table ${table} (id integer primary key)`)
const fill = (table, count) => {
const insert = db.prepare(`insert into ${table} (id) values (?)`)
for (let index = 0; index < count; index += 1) insert.run(index)
}
db.exec('begin')
fill('files', 5000)
fill('code_units', 50000)
fill('embeddings', 50000)
db.exec('commit')
db.close()
const ran = []
const { state } = harness(
{ embeddingModel: 'ollama/example:latest', embeddingDimensions: 768, stateDir },
{ shellRun: cannedShell(ran, { indexOutput: 'Indexed 0 code units from 0 files (562 unchanged, 0 removed).' }) },
)
const value = await state.tools.get('slopo').execute({ action: 'review', foreground: true }, execContext('/tmp'))
assert.equal(value.ok, true, JSON.stringify(value))
assert.equal(ran.length, 2, 'a done index goes straight to the action')
})