Files
dsh-slopo/test/plugin.test.js
dsh-slopo agent 8f68ef4e37 fix: read the settings values, not the references they arrive as
Every field is `volatile()`, which is what puts the settings card on the
Plugins page — and what makes the resolved config carry a reference per field
rather than a value. Every read was a plain-value test (`typeof x === 'string'`,
`Number(x)`, `x !== true`), so all of them were false and the plugin behaved as
if nothing had been configured.

The card displayed and persisted correctly the whole time, because the settings
wire unwraps references for display; only the plugin's own reads were broken.
Reads now go through `configValue()`, and the tool takes a fresh snapshot per
call, which is what lets an edit reach a running instance.
2026-09-26 07:13:36 +01:00

955 lines
41 KiB
JavaScript

/**
* Integration tests: load the real plugin module against a mock Cordis context,
* then drive the tool through a real shell into the real slopo CLI.
*
* The mock is deliberately thin — it implements only the seams the plugin
* actually touches — so a wrong assumption about a dsh service shows up here as
* a failing test rather than as a plugin that silently does nothing at runtime.
*/
import assert from 'node:assert/strict'
import { exec } from 'node:child_process'
import { existsSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { test } from 'node:test'
import { findSlopo } from '../lib/slopo.js'
const testHome = mkdtempSync(join(tmpdir(), 'dsh-slopo-home-'))
process.env.DSH_HOME = testHome
const { Config, apply } = await import('../index.js')
const FULL_CONFIG = {
binary: '',
autoInstall: true,
embeddingModel: '',
embeddingDimensions: 0,
embeddingApiBase: '',
embeddingApiKey: '',
embeddingParamsJson: '{}',
sourceDir: '',
sourceDirExclude: [],
ignoreFile: '',
stateDir: '',
embeddingRequestDelay: 0,
timeoutMs: 600_000,
useDefaultExcludes: true,
maxIndexUnits: 5000,
}
/**
* A shell executor shaped like `ShellExecutor` in dsh 0.1.7-rc.2: `resolve`,
* then `execute(spec)` returning a process handle whose `result()` is the
* foreground projection.
*
* There is deliberately no `run(spec)`: that single-step method was removed from
* the seam, and a plugin that still calls it fails with
* `shell.run is not a function` the first time it needs a command. Keeping the
* mock honest about the shape is the point — this file is the contract test.
*/
const shell = {
sandboxMode: undefined,
resolve: (request) => ({
workdir: process.cwd(),
timeoutMs: 600_000,
stdoutMaxBytes: 10_000_000,
...request,
}),
async execute(spec) {
const result = await new Promise((resolve) => {
exec(
spec.command,
{ cwd: spec.workdir, env: { ...process.env, ...(spec.env ?? {}) }, timeout: spec.timeoutMs, maxBuffer: 64 * 1024 * 1024 },
(error, stdout, stderr) => {
resolve({
exitCode: error === null ? 0 : typeof error.code === 'number' ? error.code : 1,
signal: null,
timedOut: Boolean(error?.killed),
aborted: false,
timeoutMs: spec.timeoutMs,
stdout: { text: String(stdout), truncated: false },
stderr: { text: String(stderr), truncated: false },
})
},
)
})
return { ...result, result: async () => result }
},
start() {
throw new Error('background process start is not used by these tests')
},
}
/**
* A stub slopo executable.
*
* Discovery itself is `test/slopo.test.js`'s business. Here the tool must never
* reach `uv tool install`: that is a network round trip, it makes the suite
* slow and flaky, and a canned runner would answer it with success without
* installing anything — leaving the plugin to report SLOPO_INSTALL_MISSING.
* Pointing `binary` at a file that exists takes the "already installed" branch,
* which is the state every one of these tests means to exercise. A test that
* wants the missing-executable path passes `binary: ''` and `autoInstall: false`.
*/
const stubSlopo = join(mkdtempSync(join(tmpdir(), 'dsh-slopo-bin-')), 'slopo')
writeFileSync(stubSlopo, '#!/bin/sh\nexit 0\n', { mode: 0o755 })
/**
* Build a context exposing exactly the services the plugin consumes.
*
* `jobs` is deliberately absent, which forces the tool down its foreground path
* so the assertions can see a result rather than a job handle.
*
* `configFromSchema` hands `apply` the config the way Cordis actually builds it
* — through `Config['~standard'].validate`, so every volatile field arrives as a
* reference object rather than a value. Without it a test can only prove the
* plugin works on a shape no real profile ever produces.
*/
function harness(
overrides = {},
{ withSettings = true, withJobs = false, withShell = true, sandboxMode, sandboxPolicyResolver, shellRun, configFromSchema = false } = {},
) {
const plain = { ...FULL_CONFIG, binary: stubSlopo, ...overrides }
const config = configFromSchema ? resolveConfig(plain) : plain
// A per-harness shell, so a test can vary sandboxMode and every resolved
// request is recorded for assertion.
const scopedShell = {
...shell,
sandboxMode,
resolve(request) {
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.
// It replaces only the low-level run, so the `execute().result()` frame
// stays exactly the shape the real seam has.
async execute(spec) {
state.shellExecutions += 1
const result = shellRun === undefined ? await shell.execute(spec).then((handle) => handle.result()) : await shellRun(spec)
return { ...result, result: async () => result }
},
}
const state = {
tools: new Map(),
updates: [],
provider: undefined,
pagePolicy: undefined,
settingsEffects: 0,
shell: scopedShell,
shellRequests: [],
shellExecutions: 0,
jobs: undefined,
}
const ctx = {
fiber: { uid: 'test-fiber' },
logger: () => ({ info() {}, warn() {}, debug() {}, error() {} }),
get(name) {
if (name === 'shell') return withShell ? scopedShell : undefined
if (name === 'jobs') return withJobs ? state.jobs : undefined
if (name === 'sandboxPolicy') return sandboxPolicyResolver
return undefined
},
inject(deps, callback) {
if (!withSettings || !deps.includes('settings')) return
// The child context `ctx.inject` hands back: its own effect scope plus
// the settings service. 0.1.7-rc.2 has no `installSection` — an entry's
// exported Config IS the form — so the only things a plugin does here are
// claim its page and write through `update`.
callback({
effect(effect) {
state.settingsEffects += 1
return effect()
},
settings: {
configure(presentation, owner) {
state.pagePolicy = presentation
state.pagePolicyOwner = owner
return () => {
state.pagePolicy = undefined
}
},
async update(namespace, patch) {
state.updates.push({ namespace, patch })
Object.assign(config, patch)
},
},
})
},
skills: {
registerProvider(create) {
state.provider = create({ signal: new AbortController().signal, invalidate() {} })
},
},
tools: {
register(definition) {
state.tools.set(definition.name, definition)
},
},
}
apply(ctx, config)
return { config, state }
}
const execContext = (cwd) => ({ signal: new AbortController().signal, agent: { id: 'session-1', session: { header: { cwd } } } })
/** Resolve a fixture the way Cordis does, volatile references and all. */
function resolveConfig(patch) {
const result = Config['~standard'].validate(patch)
assert.equal(result.issues, undefined, `the schema refused the fixture: ${JSON.stringify(result.issues)}`)
return result.value
}
test('Config is a schemastery schema, which is what cordis requires', () => {
assert.equal(typeof Config, 'function')
assert.ok(Config['~standard'] !== undefined, 'expected a Standard Schema')
})
test('apply registers exactly one tool, and it is called slopo', () => {
const { state } = harness()
assert.deepEqual([...state.tools.keys()], ['slopo'])
})
test('the ported skills are published user-invocable and NOT model-invocable', async () => {
const { state } = harness()
const candidates = await state.provider.list()
assert.deepEqual(
candidates.map((c) => c.name),
['slopo-review', 'slopo-analyze-ignore', 'slopo-analyze-one'],
)
for (const candidate of candidates) {
assert.equal(candidate.invocation.userInvocable, true, candidate.name)
assert.equal(candidate.invocation.modelInvocable, false, candidate.name)
assert.equal(candidate.rank, 600)
assert.equal(candidate.source, 'bundled')
assert.ok(candidate.description.length > 20, candidate.name)
assert.ok(candidate.locator !== undefined)
}
})
test('get() returns the skill body, routed through the locator list() issued', async () => {
const { state } = harness()
const [candidate] = await state.provider.list()
const skill = await state.provider.get(candidate)
assert.equal(skill.name, 'slopo-review')
assert.match(skill.content, /# Instructions/)
assert.match(skill.content, /First-turn output rules/)
assert.match(skill.content, /slopo.*action: "review"/s)
})
test('every skill tells the agent to call the tool rather than a shell command', async () => {
const { state } = harness()
for (const candidate of await state.provider.list()) {
const skill = await state.provider.get(candidate)
assert.match(skill.content, /`slopo` tool/, candidate.name)
// The Claude Code bash-injection directive must not survive the port.
assert.doesNotMatch(skill.content, /!`/, candidate.name)
assert.doesNotMatch(skill.content, /allowed-tools/, candidate.name)
}
})
test('the plugin claims its own settings page instead of a generated one', () => {
// `auto: false` says "this entry ships a client page" — client/client.js
// registers one onto the Plugins page. The owner must be the plugin's own
// fiber, not the settings service's.
const { state } = harness()
assert.deepEqual(state.pagePolicy, { auto: false })
assert.equal(state.pagePolicyOwner?.uid, 'test-fiber', 'the policy belongs to the plugin instance')
assert.equal(state.settingsEffects, 1, 'the policy is registered as an effect, so a reload drops it')
})
test('the namespace is the Loader entry id the bundle patch declares', async () => {
const { state } = harness()
await state.tools.get('slopo').execute({ action: 'configure', config: { embeddingModel: 'x' } }, execContext('/tmp'))
assert.equal(state.updates[0].namespace, 'dsh-slopo')
})
test('the host half no longer calls the removed installSection API', () => {
// 0.1.7-rc.2 deleted `ctx.settings.installSection`; a plugin that still calls
// it loses its whole settings surface, silently, behind a try/catch.
const source = readFileSync(new URL('../index.js', import.meta.url), 'utf8')
const code = source.replace(/\/\*[\s\S]*?\*\//g, '').replace(/^\s*\/\/.*$/gm, '')
assert.doesNotMatch(code, /installSection/)
})
test('every Config field is volatile, which is what puts it on the settings page', () => {
// The framework's form projection keeps only fields under a volatile
// ancestor, and an entry whose form is empty is dropped from
// `settings.describe` — so an unmarked field is invisible to every UI.
// The walk is over the live schema: `Config.toJSON()` is a reference
// envelope for the wire, not a tree.
const fields = Object.entries(Config.dict ?? {})
assert.ok(fields.length > 0, 'the schema must declare fields')
for (const [key, node] of fields) {
assert.equal(node.meta?.volatile, true, `${key} must be marked volatile()`)
}
})
test('the embedding API key is a declared secret, so the settings wire never carries it', () => {
assert.equal(Config.dict.embeddingApiKey.meta?.role, 'secret')
// …and no other field claims to be one, because the browser cannot read a
// secret back and every other field is one the card must display.
for (const [key, node] of Object.entries(Config.dict)) {
if (key !== 'embeddingApiKey') assert.notEqual(node.meta?.role, 'secret', `${key} must not be secret`)
}
})
test('a schema-resolved config reaches the tool as values, not as volatile references', async () => {
// The regression: Cordis hands `apply` a reference object per volatile field,
// so every plain-value read (`typeof settings.embeddingModel === 'string'`)
// was false and the configured settings were silently discarded. It looked
// healthy because `status` fell back to autodiscovery and to whatever stale
// slopo.conf.yaml was already on disk.
const { state } = harness(
{ embeddingModel: 'ollama/example:latest', embeddingDimensions: 768 },
{ configFromSchema: true },
)
const value = await state.tools.get('slopo').execute({ action: 'status' }, execContext('/tmp'))
assert.deepEqual(value.details.missingSettings, [], 'the settings must not read as unset')
assert.equal(value.ok, true)
assert.equal(value.details.embeddingModel, 'ollama/example:latest')
assert.equal(value.details.embeddingDimensions, 768)
})
test('a settings edit lands in the running tool, because the snapshot is taken per call', async () => {
// A volatile-only write does not re-apply the plugin — the Loader writes the
// new value into the reference the instance already holds — so a snapshot
// cached in `apply` would freeze this tool at its composition values forever.
const { config, state } = harness(
{ embeddingModel: 'ollama/first:latest', embeddingDimensions: 768 },
{ configFromSchema: true },
)
const tool = state.tools.get('slopo')
const before = await tool.execute({ action: 'status' }, execContext('/tmp'))
assert.match(before.message, /ollama\/first:latest/)
const [write] = Object.getOwnPropertySymbols(config.embeddingModel)
config.embeddingModel[write]('ollama/second:latest')
const after = await tool.execute({ action: 'status' }, execContext('/tmp'))
assert.match(after.message, /ollama\/second:latest/)
})
test('status reports the missing settings instead of pretending to be ready', async () => {
const { state } = harness()
const tool = state.tools.get('slopo')
const value = await tool.execute({ action: 'status' }, execContext('/tmp'))
assert.equal(value.kind, 'foreground')
assert.equal(value.ok, false)
assert.deepEqual(value.details.missingSettings, ['embedding_model', 'embedding_dimensions'])
assert.match(value.message, /Not configured yet/)
})
test('status resolves state under $DSH_HOME, never inside the workspace', async () => {
const { state } = harness()
const tool = state.tools.get('slopo')
const value = await tool.execute({ action: 'status' }, execContext('/tmp/some-repo'))
assert.ok(value.details.stateDir.startsWith(testHome), value.details.stateDir)
assert.doesNotMatch(value.details.stateDir, /^\/tmp\/some-repo/)
})
test('configure rejects a setting this plugin does not have', async () => {
const { state } = harness()
const tool = state.tools.get('slopo')
const value = await tool.execute({ action: 'configure', config: { embeddingModel: 'x', nope: 1 } }, execContext('/tmp'))
assert.equal(value.ok, false)
assert.equal(value.errorCode, 'UNKNOWN_SETTING')
assert.match(value.message, /nope/)
assert.deepEqual(state.updates, [])
})
test('configure persists through the settings namespace, not a private file', async () => {
const { state } = harness()
const tool = state.tools.get('slopo')
const value = await tool.execute(
{ action: 'configure', config: { embeddingModel: 'ollama/m:latest', embeddingDimensions: 768 } },
execContext('/tmp'),
)
assert.equal(value.ok, true)
assert.equal(state.updates.length, 1)
assert.equal(state.updates[0].namespace, 'dsh-slopo')
assert.deepEqual(state.updates[0].patch, { embeddingModel: 'ollama/m:latest', embeddingDimensions: 768 })
})
test('configure says so plainly when no settings provider is mounted', async () => {
const { state } = harness({}, { withSettings: false })
const tool = state.tools.get('slopo')
const value = await tool.execute({ action: 'configure', config: { embeddingModel: 'x' } }, execContext('/tmp'))
assert.equal(value.ok, false)
assert.equal(value.errorCode, 'NO_SETTINGS_PROVIDER')
})
test('the plugin still loads and registers its tool without a settings provider', () => {
// This is the whole reason `settings` goes through ctx.inject rather than the
// top-level inject: a required dependency would have suppressed apply entirely.
const { state } = harness({}, { withSettings: false })
assert.ok(state.tools.has('slopo'))
assert.ok(state.provider !== undefined)
})
test('review refuses to run before the workspace is configured', async () => {
const { state } = harness()
const tool = state.tools.get('slopo')
const value = await tool.execute({ action: 'review' }, execContext('/tmp'))
assert.equal(value.ok, false)
assert.equal(value.errorCode, 'NOT_CONFIGURED')
assert.match(value.remedy, /configure/)
})
test('review writes a config the plugin owns and passes the key only in the environment', async () => {
// Hermetic on purpose: `binary` points at a stub that exists, so the plugin
// takes the "already installed" branch instead of running `uv tool install`
// over the network, and the workspace is a real directory so the shell seam
// has somewhere to run. The canned runner answers both commands.
const workspace = mkdtempSync(join(tmpdir(), 'dsh-slopo-ws-'))
const binary = join(workspace, 'slopo')
writeFileSync(binary, '#!/bin/sh\nexit 0\n', { mode: 0o755 })
const ran = []
const { state } = harness(
{
binary,
embeddingModel: 'ollama/example:latest',
embeddingDimensions: 768,
embeddingApiBase: 'http://127.0.0.1:11434',
embeddingApiKey: 'super-secret',
},
{ shellRun: cannedShell(ran, { indexOutput: 'Indexed 12 code units from 3 files.' }) },
)
const tool = state.tools.get('slopo')
const value = await tool.execute({ action: 'review' }, execContext(workspace))
assert.equal(value.kind, 'foreground')
assert.ok(existsSync(value.configPath), `expected config at ${value.configPath}`)
const written = readFileSync(value.configPath, 'utf8')
assert.match(written, /embedding_model: "ollama\/example:latest"/)
assert.doesNotMatch(written, /super-secret/)
// The key rides the environment of the run, not the file.
const executions = state.shellRequests.filter((request) => /agent-review/.test(request.command))
assert.equal(executions.length, 1)
assert.equal(executions[0].env.SLOPO_EMBEDDING_API_KEY, 'super-secret')
})
test('only the warm-up backgrounds by default; the query actions return directly', async () => {
const started = []
const ran = []
const { state } = harness(
{ embeddingModel: 'ollama/example:latest', embeddingDimensions: 768 },
{ withJobs: true, shellRun: cannedShell(ran, { indexOutput: 'Indexed 1519 code units from 165 files.' }) },
)
// ctx.get('jobs') is consulted at call time, not at apply time, so the fake
// registry can be installed after the plugin has loaded.
state.jobs = {
start(spec) {
started.push(spec)
return `slopo-${started.length}`
},
}
const tool = state.tools.get('slopo')
// A review returns its report. A completion notice is best-effort — a killed
// job produces none, and an ended turn may only get the message injected — so
// the query actions must not depend on one.
const reviewed = await tool.execute({ action: 'review' }, execContext('/tmp'))
assert.equal(reviewed.kind, 'foreground')
assert.match(reviewed.report, /cluster 1/)
assert.equal(started.length, 0, 'a review must not become a job the caller has to collect')
// prepare is the long, watchable step, so it does.
const prepared = await tool.execute({ action: 'prepare' }, execContext('/tmp'))
assert.equal(prepared.kind, 'background')
assert.equal(prepared.jobId, 'slopo-1')
assert.equal(started.length, 1)
assert.equal(started[0].kind, 'slopo')
assert.equal(typeof started[0].run, 'function')
// And either can be asked for explicitly, under the framework's own name.
const forced = await tool.execute({ action: 'analyze', run_in_background: true }, execContext('/tmp'))
assert.equal(forced.kind, 'background')
assert.equal(started.length, 2)
})
test('a background job is owned by the SESSION ID and settles a JobOutcome', async () => {
// Two things the 0.1.7-rc.2 job contract requires and the old code got wrong:
// `owner` is a session id (the registry looks the agent up itself, and a job
// owned by the wrong object can be neither collected nor cancelled), and the
// producer's `done` resolves a JobOutcome whose payload field is `result` —
// `output` is the ring's word, and a job that reports to `output` settles
// with nothing for job_output to hand back.
const started = []
const ran = []
const { state } = harness(
{ embeddingModel: 'ollama/example:latest', embeddingDimensions: 768 },
{ withJobs: true, shellRun: cannedShell(ran, { indexOutput: 'Indexed 5 code units from 1 files.' }) },
)
state.jobs = {
start(spec) {
started.push(spec)
return 'slopo-1'
},
}
const value = await state.tools.get('slopo').execute({ action: 'prepare' }, execContext('/tmp'))
assert.equal(value.kind, 'background')
assert.equal(started[0].owner, 'session-1', 'ownership is fenced by session id')
const hooks = started[0].run()
assert.equal(typeof hooks.cancel, 'function')
const outcome = await hooks.done
assert.equal(outcome.status, 'completed')
assert.equal(outcome.detail, 'exit code: 0')
assert.equal(typeof outcome.result, 'string', 'the report rides `result`, not `output`')
})
test(
'review runs the real slopo CLI end to end against a real repository',
{ skip: process.env.DSH_SLOPO_E2E !== '1' || findSlopo(testHome) === undefined },
async () => {
const repo = mkdtempSync(join(tmpdir(), 'dsh-slopo-e2e-'))
const { execFileSync } = await import('node:child_process')
const { mkdirSync } = await import('node:fs')
mkdirSync(join(repo, 'src'), { recursive: true })
writeFileSync(
join(repo, 'src', 'orders.py'),
'def total_for_order(order):\n total = 0\n for line in order["lines"]:\n total += line["price"] * line["qty"]\n return round(total, 2)\n',
)
writeFileSync(
join(repo, 'src', 'invoices.py'),
'def invoice_total(invoice):\n running = 0\n for item in invoice["items"]:\n running += item["unit_price"] * item["quantity"]\n return round(running, 2)\n',
)
execFileSync('git', ['init', '-q'], { cwd: repo })
execFileSync('git', ['add', '-A'], { cwd: repo })
execFileSync('git', ['-c', 'user.email=t@t', '-c', 'user.name=t', 'commit', '-qm', 'init'], { cwd: repo })
writeFileSync(
join(repo, 'src', 'orders.py'),
'def total_for_order(order):\n total = 0\n for line in order["lines"]:\n total += line["price"] * line["qty"]\n return round(total, 2)\n\n\ndef refund_for_order(order):\n amount = 0\n for line in order["lines"]:\n amount += line["price"] * line["qty"]\n return round(amount, 2)\n',
)
const { state } = harness({
sourceDir: join(repo, 'src'),
embeddingModel: 'ollama/unclemusclez/jina-embeddings-v2-base-code:latest',
embeddingDimensions: 768,
// Where the real embedding backend lives is a property of the machine
// running the E2E suite, not of this repository, so it is read from the
// environment rather than committed. The default is Ollama's own
// loopback address.
embeddingApiBase: process.env.DSH_SLOPO_E2E_API_BASE ?? 'http://127.0.0.1:11434',
})
const tool = state.tools.get('slopo')
const prepared = await tool.execute({ action: 'prepare', run_in_background: false }, execContext(repo))
assert.equal(prepared.ok, true, JSON.stringify(prepared))
const reviewed = await tool.execute({ action: 'review' }, execContext(repo))
assert.equal(reviewed.ok, true, JSON.stringify(reviewed))
assert.match(reviewed.report, /cluster 1/)
assert.match(reviewed.report, /\* /)
},
)
test('the sandbox policy is resolved for the CALLING SESSION, not the deployment default', async () => {
// dsh-bash-sandbox stamps `request.sandboxPolicy ?? ctx.sandboxPolicy.resolve()`.
// Omitting it substitutes the DEPLOYMENT default, which is what made a
// danger-full-access session fail with SandboxUnavailableError before slopo
// was ever reached.
const seen = []
const { state } = harness(
{ embeddingModel: 'ollama/example:latest', embeddingDimensions: 768 },
{
sandboxMode: 'workspace-write',
sandboxPolicyResolver: {
resolve(arg) {
seen.push(arg)
return { mode: 'danger-full-access', workspaceRoot: '/some/workspace' }
},
},
},
)
const tool = state.tools.get('slopo')
await tool.execute({ action: 'review' }, execContext('/tmp'))
assert.equal(seen.length, 1, 'the policy must be resolved exactly once')
assert.ok(seen[0].session !== undefined, 'the calling session must travel with the policy')
// Every command the run issued carries it — not just the first. The count is
// not the assertion: how many commands a review issues depends on whether the
// index gate let it through.
assert.ok(state.shellRequests.length >= 1, 'the run must reach the shell')
for (const request of state.shellRequests) {
assert.deepEqual(request.sandboxPolicy, {
mode: 'danger-full-access',
workspaceRoot: '/some/workspace',
})
}
})
test('a confining executor with no policy service is refused rather than guessed at', async () => {
const { state } = harness(
{ embeddingModel: 'ollama/example:latest', embeddingDimensions: 768 },
{ sandboxMode: 'workspace-write' },
)
const tool = state.tools.get('slopo')
const value = await tool.execute({ action: 'review' }, execContext('/tmp'))
assert.equal(value.ok, false)
assert.equal(value.errorCode, 'NO_SANDBOX_POLICY')
assert.deepEqual(state.shellRequests, [], 'nothing may be executed')
})
test('a workspace-write session is told its state lives outside the sandbox', async () => {
const { state } = harness(
{ embeddingModel: 'ollama/example:latest', embeddingDimensions: 768 },
{
sandboxMode: 'workspace-write',
sandboxPolicyResolver: { resolve: () => ({ mode: 'workspace-write', workspaceRoot: '/some/workspace' }) },
},
)
const tool = state.tools.get('slopo')
const value = await tool.execute({ action: 'review' }, execContext('/some/workspace'))
assert.equal(value.ok, false)
assert.equal(value.errorCode, 'STATE_OUTSIDE_SANDBOX')
assert.match(value.message, /workspace-write/)
assert.match(value.remedy, /danger-full-access/)
assert.match(value.remedy, /stateDir/)
assert.deepEqual(state.shellRequests, [], 'it must not run where it cannot write')
})
test('a workspace-write session with stateDir inside the workspace proceeds', async () => {
const workspace = mkdtempSync(join(tmpdir(), 'dsh-slopo-ws-'))
const { state } = harness(
{
embeddingModel: 'ollama/example:latest',
embeddingDimensions: 768,
stateDir: join(workspace, '.slopo'),
},
{
sandboxMode: 'workspace-write',
sandboxPolicyResolver: { resolve: () => ({ mode: 'workspace-write', workspaceRoot: workspace }) },
},
)
const tool = state.tools.get('slopo')
const value = await tool.execute({ action: 'review' }, execContext(workspace))
assert.notEqual(value.errorCode, 'STATE_OUTSIDE_SANDBOX')
assert.ok(state.shellRequests.length > 0, 'it should have attempted the run')
assert.deepEqual(state.shellRequests[0].sandboxPolicy, { mode: 'workspace-write', workspaceRoot: workspace })
})
test('configure never touches the shell, and status only runs a read-only version check', async () => {
const { state } = harness({}, { sandboxMode: 'workspace-write' })
const tool = state.tools.get('slopo')
await tool.execute({ action: 'configure', config: { embeddingModel: 'x' } }, execContext('/some/workspace'))
assert.deepEqual(state.shellRequests, [], 'configure must not execute anything')
await tool.execute({ action: 'status' }, execContext('/some/workspace'))
for (const request of state.shellRequests) {
assert.match(request.command, /--version/, 'status may only run a read-only version check')
}
})
/**
* Walk a returned value and fail on anything that is not lossless JSON.
*
* `assert.deepStrictEqual` against a JSON round trip catches an `undefined`
* property (Node distinguishes `{a:1,b:undefined}` from `{a:1}`), but the walk
* names the offending path, which matters when the value is a deep diagnostic.
*/
function assertLossless(label, value) {
assert.deepEqual(value, JSON.parse(JSON.stringify(value)), `${label}: does not survive a JSON round trip`)
const walk = (node, path) => {
if (node === null || typeof node !== 'object') return
if (Array.isArray(node)) {
node.forEach((item, index) => walk(item, `${path}[${index}]`))
return
}
for (const [key, entry] of Object.entries(node)) {
assert.notEqual(entry, undefined, `${label}: ${path}.${key} is undefined, which is not lossless JSON`)
walk(entry, `${path}.${key}`)
}
}
walk(value, label)
}
test('status returns lossless JSON when the endpoint probe SUCCEEDS', async () => {
// The regression: the probe used to return `error: undefined` on a match, and
// an explicit undefined property failed the harness's output validation, so a
// perfectly healthy status call could not be serialised at all.
const { createServer } = await import('node:http')
const server = createServer((request, response) => {
response.writeHead(200, { 'content-type': 'application/json' })
response.end(JSON.stringify({ data: [{ embedding: new Array(768).fill(0.01) }] }))
})
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve))
const { port } = server.address()
try {
const { state } = harness({
embeddingModel: 'ollama/example:latest',
embeddingDimensions: 768,
embeddingApiBase: `http://127.0.0.1:${port}`,
})
const value = await state.tools.get('slopo').execute({ action: 'status' }, execContext('/tmp'))
assertLossless('status', value)
assert.equal(value.ok, true)
assert.equal(value.details.embeddingEndpoint.ok, true)
assert.equal(value.details.embeddingEndpoint.dimensions, 768)
assert.equal(Object.hasOwn(value.details.embeddingEndpoint, 'error'), false)
} finally {
await new Promise((resolve) => server.close(resolve))
}
})
test('status asks the endpoint how wide the model is when embeddingDimensions is unset', async () => {
// The question a new user cannot answer from anywhere else. The probe used to
// be gated on every required setting being present, which meant it was
// unavailable in exactly the state that needs it — so `status` said "Missing:
// embedding_dimensions" and left you to guess.
const { createServer } = await import('node:http')
const server = createServer((request, response) => {
response.writeHead(200, { 'content-type': 'application/json' })
response.end(JSON.stringify({ data: [{ embedding: new Array(1024).fill(0.01) }] }))
})
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve))
const { port } = server.address()
try {
const { state } = harness({
embeddingModel: 'ollama/example:latest',
embeddingDimensions: 0,
embeddingApiBase: `http://127.0.0.1:${port}`,
})
const value = await state.tools.get('slopo').execute({ action: 'status' }, execContext('/tmp'))
assertLossless('status (dimensions unset)', value)
assert.deepEqual(value.details.missingSettings, ['embedding_dimensions'])
assert.equal(value.details.embeddingEndpoint.checked, true, 'the probe must still run')
assert.equal(value.details.embeddingEndpoint.dimensions, 1024, 'the observed width is the answer')
assert.match(value.message, /returned 1024 dimensions/)
assert.match(value.message, /set embeddingDimensions to 1024/)
} finally {
await new Promise((resolve) => server.close(resolve))
}
})
test('status returns lossless JSON when the endpoint probe FAILS', async () => { const { state } = harness({
embeddingModel: 'ollama/example:latest',
embeddingDimensions: 768,
// Nothing is listening on this port, so the fetch rejects.
embeddingApiBase: 'http://127.0.0.1:1',
})
const value = await state.tools.get('slopo').execute({ action: 'status' }, execContext('/tmp'))
assertLossless('status (unreachable)', value)
assert.equal(value.ok, false)
assert.ok(value.details.embeddingEndpoint.error.length > 0)
})
test('every action that returns without executing anything is lossless JSON', async () => {
const cases = [
[{ action: 'status' }, 'status (unconfigured)'],
[{ action: 'configure', config: { embeddingModel: 'x', nope: 1 } }, 'configure (unknown setting)'],
[{ action: 'configure', config: { embeddingModel: 'x' } }, 'configure (ok)'],
[{ action: 'configure', config: 'not-an-object' }, 'configure (bad patch)'],
[{ action: 'review' }, 'review (unconfigured)'],
]
for (const [args, label] of cases) {
const { state } = harness()
const value = await state.tools.get('slopo').execute(args, execContext('/tmp'))
assertLossless(label, value)
}
})
test('an action refused before it runs is lossless JSON', async () => {
const configured = {
embeddingModel: 'ollama/example:latest',
embeddingDimensions: 768,
embeddingApiKey: 'secret',
embeddingApiBase: 'http://127.0.0.1:11434',
}
const noShell = harness(configured, { withShell: false })
const refused = await noShell.state.tools.get('slopo').execute({ action: 'review' }, execContext('/tmp'))
assertLossless('review (no shell)', refused)
assert.equal(refused.errorCode, 'NO_SHELL')
const outside = harness(configured, {
sandboxMode: 'workspace-write',
sandboxPolicyResolver: { resolve: () => ({ mode: 'workspace-write', workspaceRoot: '/some/workspace' }) },
})
const value = await outside.state.tools.get('slopo').execute({ action: 'review' }, execContext('/some/workspace'))
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' }, 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' }, 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' }, 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' }, 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', run_in_background: false }, 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', run_in_background: false }, 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' }, execContext('/tmp'))
assert.equal(value.ok, true, JSON.stringify(value))
assert.equal(ran.length, 2, 'a done index goes straight to the action')
})