Three test-only spots had baked in the machine this repository was developed
on, and the repository is publicly readable, so they shipped that machine's
shape to anyone who cloned it:
* the end-to-end embedding backend was pinned to a private LAN address;
it is now read from DSH_SLOPO_E2E_API_BASE, defaulting to Ollama's own
loopback address, so the machine running the suite supplies its own;
* the real-slopo test resolved discovery from a hardcoded root home, which
meant it could only ever run for one user — it resolves from the running
user's home now, which is also the fix for everyone else;
* the harness-slot contract tests read the global npm tree by absolute
path; they go through this repository's own node_modules symlink, which
is already relative to the checkout.
No user-facing behaviour changes and the published package never contained
the test files, so this push publishes nothing. The default configuration was
already free of machine-specific values: every address, path and key defaults
to empty and is resolved per user at runtime.
[skip release]
187 lines
8.2 KiB
JavaScript
187 lines
8.2 KiB
JavaScript
import assert from 'node:assert/strict'
|
|
import { execFileSync } 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 { renderSlopoConfig } from '../lib/config.js'
|
|
import {
|
|
commandLine,
|
|
explainFailure,
|
|
findSlopo,
|
|
installEnv,
|
|
lastLogError,
|
|
shellQuote,
|
|
slopoArgs,
|
|
slopoCandidates,
|
|
uvInstallArgs,
|
|
} from '../lib/slopo.js'
|
|
import { managedSlopoPath, resolveDshHome, userLocalSlopoPath, workspaceKey, workspaceStateDir } from '../lib/paths.js'
|
|
|
|
test('findSlopo prefers the managed install over PATH', () => {
|
|
const home = '/dsh'
|
|
const env = { PATH: '/usr/bin:/bin' }
|
|
const candidates = slopoCandidates(home, env)
|
|
const managed = candidates.indexOf(managedSlopoPath(home))
|
|
const onPath = candidates.indexOf('/usr/bin/slopo')
|
|
assert.ok(managed >= 0 && onPath >= 0)
|
|
assert.ok(managed < onPath, 'managed install must be probed before PATH')
|
|
assert.equal(findSlopo(home, env, (p) => p === '/bin/slopo'), '/bin/slopo')
|
|
assert.equal(findSlopo(home, env, () => false), undefined)
|
|
})
|
|
|
|
test('findSlopo honours an explicit binary, including one not on PATH', () => {
|
|
const env = { PATH: '', DSH_SLOPO_BINARY: '/opt/custom/slopo' }
|
|
assert.equal(findSlopo('/dsh', env, (p) => p === '/opt/custom/slopo'), '/opt/custom/slopo')
|
|
})
|
|
|
|
test('installEnv redirects all three uv directories into the harness home', () => {
|
|
const env = installEnv('/dsh', { PATH: '/usr/bin' })
|
|
assert.equal(env.UV_TOOL_DIR, '/dsh/dsh-slopo/uv-tools')
|
|
assert.equal(env.UV_TOOL_BIN_DIR, '/dsh/dsh-slopo/uv-tools/bin')
|
|
assert.equal(env.UV_PYTHON_INSTALL_DIR, '/dsh/dsh-slopo/uv-python')
|
|
// The parent environment must survive, or PATH is lost for the install.
|
|
assert.equal(env.PATH, '/usr/bin')
|
|
})
|
|
|
|
test('uvInstallArgs pins the interpreter slopo requires', () => {
|
|
assert.deepEqual(uvInstallArgs(), ['tool', 'install', 'slopo', '--python', '3.12'])
|
|
assert.deepEqual(uvInstallArgs({ upgrade: true }), ['tool', 'install', 'slopo', '--python', '3.12', '--upgrade'])
|
|
assert.deepEqual(uvInstallArgs({ version: 'v0.8.0' }), ['tool', 'install', 'slopo==0.8.0', '--python', '3.12'])
|
|
})
|
|
|
|
test('shellQuote survives a single quote in a path', () => {
|
|
assert.equal(shellQuote("/tmp/it's here"), `'/tmp/it'\\''s here'`)
|
|
assert.equal(commandLine(['/bin/x', 'a b']), `'/bin/x' 'a b'`)
|
|
})
|
|
|
|
test('slopoArgs puts the global --config before the subcommand', () => {
|
|
// typer only accepts a callback option ahead of the command; verified against slopo 0.8.0.
|
|
for (const action of ['review', 'analyze', 'index', 'show-config']) {
|
|
const args = slopoArgs('/c.yaml', action)
|
|
assert.equal(args[0], '--config')
|
|
assert.equal(args[1], '/c.yaml')
|
|
assert.ok(args.findIndex((a) => a.startsWith('agent-') || a === 'index' || a === 'show-config') > 1)
|
|
}
|
|
})
|
|
|
|
test('--config-version is a subcommand option, so it must FOLLOW the subcommand', () => {
|
|
// Found the hard way: slopo exits 2 (a typer usage error) when --config-version
|
|
// precedes the command, because it is declared on the command, not the callback.
|
|
for (const action of ['review', 'analyze', 'analyze-one']) {
|
|
const args = slopoArgs('/c.yaml', action)
|
|
const commandAt = args.findIndex((a) => a.startsWith('agent-'))
|
|
const versionAt = args.indexOf('--config-version')
|
|
assert.ok(commandAt > 0, action)
|
|
assert.ok(versionAt > commandAt, `${action}: --config-version must follow the subcommand`)
|
|
}
|
|
})
|
|
|
|
test('slopoArgs passes the config-version alarm on agent commands only', () => {
|
|
assert.ok(slopoArgs('/c.yaml', 'review').includes('--config-version'))
|
|
// index/embed are not agent commands and take no version flag.
|
|
assert.ok(!slopoArgs('/c.yaml', 'index').includes('--config-version'))
|
|
})
|
|
|
|
test('slopoArgs maps arguments, and omits them when empty', () => {
|
|
assert.deepEqual(slopoArgs('/c.yaml', 'review', { base: 'main' }).slice(-2), ['--base', 'main'])
|
|
assert.ok(!slopoArgs('/c.yaml', 'review', { base: '' }).includes('--base'))
|
|
assert.deepEqual(slopoArgs('/c.yaml', 'analyze-one', { cluster: 'abc' }).slice(-3), ['--single', '--cluster', 'abc'])
|
|
assert.ok(!slopoArgs('/c.yaml', 'analyze-one').includes('--cluster'))
|
|
})
|
|
|
|
test('slopoArgs refuses an unknown action rather than running something else', () => {
|
|
assert.throws(() => slopoArgs('/c.yaml', 'delete-everything'), /unknown slopo action/)
|
|
})
|
|
|
|
test('explainFailure turns each observed slopo message into an action', () => {
|
|
const cases = [
|
|
['Error: No data in database. The tool wasn\'t initialized.', 'DATABASE_MISSING'],
|
|
['Error: Configuration mismatch.', 'CONFIGURATION_MISMATCH'],
|
|
['Error: Too many code units to embed. The tool probably wasn\'t fully initialized.', 'BACKLOG_TOO_LARGE'],
|
|
['Error: Configuration file not found. Make sure that the agent CLI is run from the correct directory.', 'CONFIG_MISSING'],
|
|
['Error: Loading configuration failed.', 'CONFIG_INVALID'],
|
|
['Error: Index is out of date. Run `index` and `embed` first.', 'INDEX_STALE'],
|
|
['Error: Git command failed.', 'GIT_FAILED'],
|
|
['Error: Embedding failed.', 'EMBEDDING_FAILED'],
|
|
['Error: something nobody has seen before', 'SLOPO_FAILED'],
|
|
]
|
|
for (const [stderr, code] of cases) {
|
|
const explained = explainFailure({ stderr, stdout: '' })
|
|
assert.equal(explained.code, code, stderr)
|
|
assert.ok(explained.message.length > 0)
|
|
assert.ok(explained.remedy.length > 0)
|
|
}
|
|
})
|
|
|
|
test('explainFailure reads the real detail out of the log, not just stderr', () => {
|
|
// This is the genuine shape: stderr is deliberately terse, the log is not.
|
|
const explained = explainFailure({
|
|
stderr: 'Error: Embedding failed.',
|
|
stdout: '',
|
|
logDetail: 'Error: litellm.UnsupportedParamsError: Setting dimensions is not supported',
|
|
})
|
|
assert.equal(explained.code, 'EMBEDDING_FAILED')
|
|
})
|
|
|
|
test('lastLogError returns the most recent error line', () => {
|
|
const log = [
|
|
'==== 2026-01-01 00:00:00 agent-review ====',
|
|
'Error: first problem',
|
|
'> index',
|
|
'Indexed 3 code units',
|
|
'Error: litellm.UnsupportedParamsError: Setting dimensions is not supported',
|
|
].join('\n')
|
|
assert.match(lastLogError(log), /UnsupportedParamsError/)
|
|
assert.equal(lastLogError('no errors here'), '')
|
|
})
|
|
|
|
test('workspaceKey is stable, name-bearing and collision-resistant', () => {
|
|
assert.equal(workspaceKey('/a/b/app'), workspaceKey('/a/b/app'))
|
|
assert.notEqual(workspaceKey('/a/b/app'), workspaceKey('/x/y/app'))
|
|
assert.match(workspaceKey('/a/b/app'), /^app-[0-9a-f]{8}$/)
|
|
})
|
|
|
|
test('resolveDshHome prefers $DSH_HOME and rejects a blank one', () => {
|
|
assert.equal(resolveDshHome({ DSH_HOME: '/persist/.dsh' }), '/persist/.dsh')
|
|
assert.match(resolveDshHome({ DSH_HOME: ' ' }), /\.dsh$/)
|
|
})
|
|
|
|
test('workspace state lives under the harness home, not next to the code', () => {
|
|
assert.match(workspaceStateDir('/dsh', '/repo'), /^\/dsh\/dsh-slopo\/workspaces\/repo-[0-9a-f]{8}$/)
|
|
})
|
|
|
|
test(
|
|
'the generated config is accepted by the real slopo',
|
|
{
|
|
// Discovery resolves from the RUNNING user's home, not from a committed
|
|
// absolute one: a hardcoded home directory only ever matched one machine,
|
|
// and it published that machine's layout to everyone else.
|
|
skip: !existsSync(managedSlopoPath(resolveDshHome())) && !existsSync(userLocalSlopoPath()),
|
|
},
|
|
() => {
|
|
const binary = findSlopo(resolveDshHome())
|
|
assert.ok(binary, 'expected a slopo binary for this test')
|
|
const dir = mkdtempSync(join(tmpdir(), 'dsh-slopo-conf-'))
|
|
const configPath = join(dir, 'slopo.conf.yaml')
|
|
writeFileSync(
|
|
configPath,
|
|
renderSlopoConfig({
|
|
source_dir: dir,
|
|
db_file: join(dir, 'slopo.db'),
|
|
ignore_file: join(dir, 'slopo.ignore.txt'),
|
|
agent_log_file: join(dir, 'slopo.agent.log'),
|
|
embedding_model: 'ollama/example:latest',
|
|
embedding_dimensions: 768,
|
|
embedding_params: { api_base: 'http://127.0.0.1:11434' },
|
|
source_dir_exclude: ['**/test/**'],
|
|
}),
|
|
)
|
|
const out = execFileSync(binary, ['--config', configPath, 'show-config'], { encoding: 'utf8' })
|
|
assert.match(out, /embedding_model: ollama\/example:latest/)
|
|
assert.match(out, /embedding_dimensions: 768/)
|
|
assert.match(out, /api_base: http:\/\/127\.0\.0\.1:11434/)
|
|
},
|
|
)
|