/** * 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. */ function harness(overrides = {}, { withSettings = true, withJobs = false, withShell = true, sandboxMode, sandboxPolicyResolver, shellRun } = {}) { const config = { ...FULL_CONFIG, binary: stubSlopo, ...overrides } // 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 } } } }) 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('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, embeddingApiBase: 'http://10.50.1.2: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 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') })