Files
dsh-slopo/test/plugin.test.js
T
dsh-slopo agent 46124b11aa Fix: status could not be serialised when the probe SUCCEEDED
The probe returned `error: undefined` on a dimension match. `undefined` is not a
JSON value -- JSON.stringify silently drops it from an object -- so the shape
looked harmless and was not: the harness enforces the declared output schema
against the canonical value before materialising it, and an explicit undefined
property fails that check. A perfectly healthy status call could not be returned
at all.

Fixed at the boundary rather than at each site. Every return now funnels through
`toJsonValue`, which drops undefined properties exactly as JSON.stringify would,
maps non-finite numbers to null (same reasoning: JSON.stringify(NaN) is "null"),
turns an undefined array slot into null rather than shifting indices, and refuses
a circular value instead of recursing forever. `update` was carrying
`errorCode: undefined` and `ensureSlopo` a `detail: undefined` on the same path,
so a per-site fix would have left two more of these waiting.

The probe also now omits `error` rather than assigning undefined, since it is a
plausible thing for another consumer to read.

Four regression tests, and they were checked against a negative control: with the
sanitiser reverted and the undefined restored, exactly the "probe SUCCEEDS" test
fails with "does not survive a JSON round trip" and the others still pass, which
is what makes it a test of this bug rather than of the JSON round trip in
general.
2026-09-22 13:08:22 +01:00

536 lines
21 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, 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,
}
/** A shell executor that really runs the command. */
const shell = {
sandboxMode: undefined,
resolve: (request) => ({
workdir: process.cwd(),
timeoutMs: 600_000,
stdoutMaxBytes: 10_000_000,
...request,
}),
run(spec) {
return 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 },
})
},
)
})
},
start() {
throw new Error('background process start is not used by these tests')
},
}
/**
* 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 } = {}) {
const config = { ...FULL_CONFIG, ...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 }
},
}
const state = {
tools: new Map(),
updates: [],
provider: undefined,
namespace: undefined,
shell: scopedShell,
shellRequests: [],
jobs: undefined,
}
const ctx = {
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
callback({
settings: {
installSection(owner, namespace, schema, entry, hooks) {
state.namespace = namespace
state.schema = schema
hooks.setSource(() => config)
},
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: { 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 settings namespace is registered under a grammar-valid name', () => {
const { state } = harness()
assert.match(state.namespace, /^[a-z][a-z0-9-]*$/)
})
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 () => {
const { state } = harness({
embeddingModel: 'ollama/example:latest',
embeddingDimensions: 768,
embeddingApiBase: 'http://127.0.0.1:11434',
embeddingApiKey: 'super-secret',
})
const tool = state.tools.get('slopo')
const value = await tool.execute({ action: 'review', foreground: true }, execContext('/tmp/nonexistent-repo-xyz'))
// It will fail — there is no git repo there — but the failure must be a
// reported one, and the config must have been materialised first.
assert.equal(value.kind, 'foreground')
assert.ok(existsSync(value.configPath), `expected config at ${value.configPath}`)
const { readFileSync } = await import('node:fs')
const written = readFileSync(value.configPath, 'utf8')
assert.match(written, /embedding_model: "ollama\/example:latest"/)
assert.doesNotMatch(written, /super-secret/)
})
test('a background-capable profile gets a job handle instead of a blocking call', async () => {
const started = []
const { state } = harness(
{ embeddingModel: 'ollama/example:latest', embeddingDimensions: 768 },
{ withJobs: true },
)
// 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-1'
},
}
const tool = state.tools.get('slopo')
const value = await tool.execute({ action: 'review' }, execContext('/tmp'))
assert.equal(value.kind, 'background')
assert.equal(value.jobId, 'slopo-1')
assert.equal(started.length, 1)
assert.equal(started[0].kind, 'slopo')
assert.equal(typeof started[0].run, 'function')
})
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', foreground: true }, execContext(repo))
assert.equal(prepared.ok, true, JSON.stringify(prepared))
const reviewed = await tool.execute({ action: 'review', foreground: true }, 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', foreground: true }, execContext('/tmp/nonexistent-repo-xyz'))
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')
assert.equal(state.shellRequests.length, 1)
assert.deepEqual(state.shellRequests[0].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', foreground: true }, 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')
})