213 lines
7.8 KiB
JavaScript
213 lines
7.8 KiB
JavaScript
|
|
// Unit tests for src/main/plugin-validation.js — static overlay validation.
|
||
|
|
// Uses in-memory fixtures so no dev-clone is required.
|
||
|
|
|
||
|
|
'use strict'
|
||
|
|
|
||
|
|
const test = require('node:test')
|
||
|
|
const assert = require('node:assert/strict')
|
||
|
|
const fs = require('node:fs')
|
||
|
|
const os = require('node:os')
|
||
|
|
const path = require('node:path')
|
||
|
|
|
||
|
|
const V = require('../src/main/plugin-validation.js')
|
||
|
|
|
||
|
|
// A small stand-in for `deepseek-harness-dev/packages/`. We create one group
|
||
|
|
// with two publishable packages so the workspace scanner has real work.
|
||
|
|
function makeFakePackagesRoot() {
|
||
|
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'dsh-validation-'))
|
||
|
|
const bash = path.join(root, 'bash', 'bash-local')
|
||
|
|
const fsGroup = path.join(root, 'fs', 'fs-local')
|
||
|
|
const stray = path.join(root, 'stray')
|
||
|
|
fs.mkdirSync(bash, { recursive: true })
|
||
|
|
fs.mkdirSync(fsGroup, { recursive: true })
|
||
|
|
fs.mkdirSync(stray, { recursive: true })
|
||
|
|
fs.writeFileSync(path.join(bash, 'package.json'), JSON.stringify({ name: '@deepseek-ai/dsh-bash-local' }))
|
||
|
|
fs.writeFileSync(path.join(fsGroup, 'package.json'), JSON.stringify({ name: '@deepseek-ai/dsh-fs-local' }))
|
||
|
|
// stray/ has no package.json — the scanner must silently skip it.
|
||
|
|
return root
|
||
|
|
}
|
||
|
|
|
||
|
|
test('scanWorkspacePackages picks up @deepseek-ai/dsh-* names, skips dirs without package.json', () => {
|
||
|
|
const root = makeFakePackagesRoot()
|
||
|
|
try {
|
||
|
|
const names = V.scanWorkspacePackages(root)
|
||
|
|
assert.ok(names.has('@deepseek-ai/dsh-bash-local'))
|
||
|
|
assert.ok(names.has('@deepseek-ai/dsh-fs-local'))
|
||
|
|
assert.equal(names.size, 2)
|
||
|
|
} finally {
|
||
|
|
fs.rmSync(root, { recursive: true, force: true })
|
||
|
|
}
|
||
|
|
})
|
||
|
|
|
||
|
|
test('scanWorkspacePackages: missing dir returns empty set (no throw)', () => {
|
||
|
|
const names = V.scanWorkspacePackages('/definitely/does/not/exist')
|
||
|
|
assert.equal(names.size, 0)
|
||
|
|
})
|
||
|
|
|
||
|
|
test('classifyName recognises packages, relative paths, and absolute paths', () => {
|
||
|
|
assert.equal(V.classifyName('@deepseek-ai/dsh-bash-local'), 'package')
|
||
|
|
assert.equal(V.classifyName('cordis'), 'package')
|
||
|
|
assert.equal(V.classifyName('../../foo/bar.ts'), 'relative-path')
|
||
|
|
assert.equal(V.classifyName('./inline.ts'), 'relative-path')
|
||
|
|
assert.equal(V.classifyName('/etc/passwd'), 'absolute-path')
|
||
|
|
assert.equal(V.classifyName(''), 'unknown')
|
||
|
|
})
|
||
|
|
|
||
|
|
test('packageResolves: known package passes', () => {
|
||
|
|
const known = new Set(['@deepseek-ai/dsh-bash-local'])
|
||
|
|
const reason = V.packageResolves(
|
||
|
|
{ id: 'bash', name: '@deepseek-ai/dsh-bash-local' },
|
||
|
|
{ knownPackages: known, leafDir: '/tmp' },
|
||
|
|
)
|
||
|
|
assert.equal(reason, null)
|
||
|
|
})
|
||
|
|
|
||
|
|
test('packageResolves: unknown package fails with a helpful message', () => {
|
||
|
|
const known = new Set(['@deepseek-ai/dsh-fs-local'])
|
||
|
|
const reason = V.packageResolves(
|
||
|
|
{ id: 'bash', name: '@deepseek-ai/dsh-bash-local' },
|
||
|
|
{ knownPackages: known, leafDir: '/tmp' },
|
||
|
|
)
|
||
|
|
assert.match(reason, /package not found/)
|
||
|
|
})
|
||
|
|
|
||
|
|
test('packageResolves: relative path resolves against leafDir', () => {
|
||
|
|
const home = fs.mkdtempSync(path.join(os.tmpdir(), 'dsh-validation-'))
|
||
|
|
try {
|
||
|
|
const target = path.join(home, 'nested', 'plugin.ts')
|
||
|
|
fs.mkdirSync(path.dirname(target), { recursive: true })
|
||
|
|
fs.writeFileSync(target, '// hi')
|
||
|
|
const reason = V.packageResolves(
|
||
|
|
{ id: 'x', name: './nested/plugin.ts' },
|
||
|
|
{ knownPackages: new Set(), leafDir: home },
|
||
|
|
)
|
||
|
|
assert.equal(reason, null)
|
||
|
|
} finally {
|
||
|
|
fs.rmSync(home, { recursive: true, force: true })
|
||
|
|
}
|
||
|
|
})
|
||
|
|
|
||
|
|
test('packageResolves: missing file surfaces the path', () => {
|
||
|
|
const reason = V.packageResolves(
|
||
|
|
{ id: 'x', name: './does-not-exist.ts' },
|
||
|
|
{ knownPackages: new Set(), leafDir: '/tmp' },
|
||
|
|
)
|
||
|
|
assert.match(reason, /file does not exist/)
|
||
|
|
})
|
||
|
|
|
||
|
|
test('editDistance handles identical, empty, and typical inputs', () => {
|
||
|
|
assert.equal(V.editDistance('abc', 'abc'), 0)
|
||
|
|
assert.equal(V.editDistance('', 'abc'), 3)
|
||
|
|
assert.equal(V.editDistance('kitten', 'sitting'), 3)
|
||
|
|
assert.equal(V.editDistance('bash', 'bosh'), 1)
|
||
|
|
})
|
||
|
|
|
||
|
|
test('validate: clean base + no patches → no diagnostics', () => {
|
||
|
|
const diags = V.validate({
|
||
|
|
baseEntries: [
|
||
|
|
{ id: 'bash', name: '@deepseek-ai/dsh-bash-local', line: 1 },
|
||
|
|
{ id: 'fs', name: '@deepseek-ai/dsh-fs-local', line: 3 },
|
||
|
|
],
|
||
|
|
overlay: { base: '', patches: [] },
|
||
|
|
knownPackages: new Set(['@deepseek-ai/dsh-bash-local', '@deepseek-ai/dsh-fs-local']),
|
||
|
|
leafDir: '/tmp',
|
||
|
|
})
|
||
|
|
assert.equal(diags.length, 0)
|
||
|
|
})
|
||
|
|
|
||
|
|
test('validate: unknown package on a base entry → error diagnostic with line', () => {
|
||
|
|
const diags = V.validate({
|
||
|
|
baseEntries: [{ id: 'bash', name: '@deepseek-ai/does-not-exist', line: 7 }],
|
||
|
|
overlay: { base: '', patches: [] },
|
||
|
|
knownPackages: new Set(),
|
||
|
|
leafDir: '/tmp',
|
||
|
|
})
|
||
|
|
const err = diags.find((d) => d.severity === 'error' && d.id === 'bash')
|
||
|
|
assert.ok(err)
|
||
|
|
assert.equal(err.line, 7)
|
||
|
|
assert.match(err.message, /package not found/)
|
||
|
|
})
|
||
|
|
|
||
|
|
test('validate: patch targets an id absent from base → error diagnostic', () => {
|
||
|
|
const diags = V.validate({
|
||
|
|
baseEntries: [{ id: 'bash', name: '@deepseek-ai/dsh-bash-local' }],
|
||
|
|
overlay: { base: '', patches: [{ id: 'ghost', disabled: true, line: 12 }] },
|
||
|
|
knownPackages: new Set(['@deepseek-ai/dsh-bash-local']),
|
||
|
|
leafDir: '/tmp',
|
||
|
|
})
|
||
|
|
const err = diags.find((d) => d.scope === 'patch' && d.id === 'ghost')
|
||
|
|
assert.ok(err)
|
||
|
|
assert.equal(err.line, 12)
|
||
|
|
assert.match(err.message, /not in the base leaf/)
|
||
|
|
})
|
||
|
|
|
||
|
|
test('validate: new-entry patch with an unknown package → error diagnostic', () => {
|
||
|
|
const diags = V.validate({
|
||
|
|
baseEntries: [{ id: 'bash', name: '@deepseek-ai/dsh-bash-local' }],
|
||
|
|
overlay: { base: '', patches: [{ id: 'custom', name: '@nope/plugin', line: 20 }] },
|
||
|
|
knownPackages: new Set(['@deepseek-ai/dsh-bash-local']),
|
||
|
|
leafDir: '/tmp',
|
||
|
|
})
|
||
|
|
const err = diags.find((d) => d.id === 'custom')
|
||
|
|
assert.ok(err)
|
||
|
|
assert.match(err.message, /package not found/)
|
||
|
|
})
|
||
|
|
|
||
|
|
test('validate: near-duplicate active ids trigger a warn (not error)', () => {
|
||
|
|
const diags = V.validate({
|
||
|
|
baseEntries: [
|
||
|
|
{ id: 'bash-local', name: '@deepseek-ai/dsh-bash-local' },
|
||
|
|
{ id: 'bash-locel', name: '@deepseek-ai/dsh-bash-local' }, // typo
|
||
|
|
],
|
||
|
|
overlay: { base: '', patches: [] },
|
||
|
|
knownPackages: new Set(['@deepseek-ai/dsh-bash-local']),
|
||
|
|
leafDir: '/tmp',
|
||
|
|
})
|
||
|
|
const near = diags.find((d) => d.severity === 'warn' && /near-duplicate/.test(d.message))
|
||
|
|
assert.ok(near)
|
||
|
|
})
|
||
|
|
|
||
|
|
test('validate: disabled entries are excluded from near-duplicate check', () => {
|
||
|
|
const diags = V.validate({
|
||
|
|
baseEntries: [
|
||
|
|
{ id: 'bash-local', name: '@deepseek-ai/dsh-bash-local' },
|
||
|
|
{ id: 'bash-locel', name: '@deepseek-ai/dsh-bash-local' },
|
||
|
|
],
|
||
|
|
overlay: { base: '', patches: [{ id: 'bash-locel', disabled: true }] },
|
||
|
|
knownPackages: new Set(['@deepseek-ai/dsh-bash-local']),
|
||
|
|
leafDir: '/tmp',
|
||
|
|
})
|
||
|
|
const near = diags.find((d) => /near-duplicate/.test(d.message))
|
||
|
|
assert.equal(near, undefined)
|
||
|
|
})
|
||
|
|
|
||
|
|
test('validate: tool-count warning at configurable threshold', () => {
|
||
|
|
const many = []
|
||
|
|
for (let i = 0; i < 5; i++) many.push({ id: `p${i}`, name: '@deepseek-ai/dsh-bash-local' })
|
||
|
|
const diags = V.validate({
|
||
|
|
baseEntries: many,
|
||
|
|
overlay: { base: '', patches: [] },
|
||
|
|
knownPackages: new Set(['@deepseek-ai/dsh-bash-local']),
|
||
|
|
leafDir: '/tmp',
|
||
|
|
toolCountWarnAt: 3,
|
||
|
|
})
|
||
|
|
const w = diags.find((d) => /enabled entries/.test(d.message))
|
||
|
|
assert.ok(w)
|
||
|
|
assert.equal(w.severity, 'warn')
|
||
|
|
})
|
||
|
|
|
||
|
|
test('validate: duplicate id in base → error', () => {
|
||
|
|
const diags = V.validate({
|
||
|
|
baseEntries: [
|
||
|
|
{ id: 'bash', name: '@deepseek-ai/dsh-bash-local', line: 1 },
|
||
|
|
{ id: 'bash', name: '@deepseek-ai/dsh-bash-local', line: 5 },
|
||
|
|
],
|
||
|
|
overlay: { base: '', patches: [] },
|
||
|
|
knownPackages: new Set(['@deepseek-ai/dsh-bash-local']),
|
||
|
|
leafDir: '/tmp',
|
||
|
|
})
|
||
|
|
const dup = diags.find((d) => /duplicate id/.test(d.message))
|
||
|
|
assert.ok(dup)
|
||
|
|
assert.equal(dup.severity, 'error')
|
||
|
|
})
|