fix(review): validate skill roots at mount and isolate provider default roots
ds-review-bot round 1 on the repository-plugin runtime: - a manifest-declared skill root absent or non-directory in the installed package now fails the plugin load (skill-local treats a missing root as legitimately empty, which silently mounted a skill-less plugin) - includeDefaultRoots: false no longer inherits $DSH_BUNDLED_SKILL_DIR, so isolated repository providers see only their explicit roots - prepared wrapper baseUrl schema requires the file: scheme, failing hostile URLs at the declared validation boundary - preparedPath reuses format.ts's isOutside; SERVER_NAME_PATTERN is exported and pinned equal to dsh-mcp-client's, with the restatement justified (the prepare bin keeps a zod-only module graph); the unexplained `as never` cast now carries its schemastery rationale - the import-free wrapper assertion also rejects dynamic import( - the headless fixture wrapper is regenerated by the real prepareDshPlugin and a drift test pins fixture == generator output - prepareDshPlugin JSDoc states the non-atomic publish repair contract
This commit is contained in:
@@ -1,7 +1,15 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { parseMcpDocument, resolveMcpServers } from '../src/mcp.ts'
|
||||
import { SERVER_NAME_PATTERN as CLIENT_SERVER_NAME_PATTERN } from '@deepseek-ai/dsh-mcp-client'
|
||||
import { SERVER_NAME_PATTERN, parseMcpDocument, resolveMcpServers } from '../src/mcp.ts'
|
||||
|
||||
describe('repository plugin common .mcp.json support', () => {
|
||||
it('validates server names with exactly the pattern the MCP client registry enforces', () => {
|
||||
// mcp.ts restates the pattern to keep the prepare bin's module graph
|
||||
// zod-only; this pin is the drift guard.
|
||||
expect(SERVER_NAME_PATTERN.source).toBe(CLIENT_SERVER_NAME_PATTERN.source)
|
||||
expect(SERVER_NAME_PATTERN.flags).toBe(CLIENT_SERVER_NAME_PATTERN.flags)
|
||||
})
|
||||
|
||||
it('maps Expo-style HTTP servers to the existing Streamable HTTP client config', () => {
|
||||
const document = parseMcpDocument(JSON.stringify({
|
||||
mcpServers: {
|
||||
|
||||
@@ -59,7 +59,9 @@ describe('dsh-plugin-prepare', () => {
|
||||
})
|
||||
const wrapper = await readFile(join(directory, RepositoryPlugin.PREPARED_ENTRY_FILENAME), 'utf8')
|
||||
expect(wrapper).toContain(`ctx.loader.builtins["${RepositoryPlugin.REPOSITORY_PLUGIN_BUILTIN}"]`)
|
||||
expect(wrapper).not.toMatch(/\b(?:import|from)\s/)
|
||||
// Import-free means no static AND no dynamic imports; `import.meta.url`
|
||||
// (no whitespace, no call parenthesis) is the one allowed appearance.
|
||||
expect(wrapper).not.toMatch(/\b(?:import|from)\s|\bimport\s*\(/)
|
||||
await expect(readFile(join(directory, 'dsh-plugin-assets/skills/0/repository-fixture/SKILL.md'), 'utf8'))
|
||||
.resolves.toContain('Static instructions.')
|
||||
await expect(readFile(join(directory, 'dsh-plugin-assets/.mcp.json'), 'utf8'))
|
||||
@@ -218,6 +220,34 @@ describe('prepared repository plugin Loader composition', () => {
|
||||
await ctx.fiber.dispose()
|
||||
})
|
||||
|
||||
it('fails the plugin load when a declared skill root is missing or not a directory', async () => {
|
||||
const root = await temporaryDirectory('missing-skill-root')
|
||||
await writeFile(join(root, 'not-a-directory'), 'text')
|
||||
const ctx = new Context()
|
||||
ctx.baseUrl = pathToFileURL(root).href + '/'
|
||||
await ctx.plugin(Loader)
|
||||
await ctx.plugin(SkillService)
|
||||
await ctx.plugin(RepositoryPlugin)
|
||||
|
||||
for (const [filename, skillPath, message] of [
|
||||
['missing.mjs', 'dsh-plugin-assets/skills/0', 'skill root is missing from the installed package'],
|
||||
['file.mjs', 'not-a-directory', 'skill root is not a directory'],
|
||||
] as const) {
|
||||
const wrapper = join(root, filename)
|
||||
await writeFile(wrapper, [
|
||||
"export const inject = ['loader']",
|
||||
'export async function apply(ctx) {',
|
||||
` await ctx.plugin(ctx.loader.builtins['${RepositoryPlugin.REPOSITORY_PLUGIN_BUILTIN}'], {`,
|
||||
` baseUrl: import.meta.url, manifest: { name: 'damaged', skills: [${JSON.stringify(skillPath)}] },`,
|
||||
' })',
|
||||
'}',
|
||||
'',
|
||||
].join('\n'))
|
||||
await expect(ctx.loader.create({ name: pathToFileURL(wrapper).href })).rejects.toThrow(message)
|
||||
}
|
||||
await ctx.fiber.dispose()
|
||||
})
|
||||
|
||||
it('rejects duplicate builtin ownership and preserves a later replacement on teardown', async () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(Loader)
|
||||
|
||||
Reference in New Issue
Block a user