feat(repository-plugin): load trusted package code

This commit is contained in:
Tianyi Cui
2026-08-08 19:25:14 +08:00
parent a0c64f4906
commit 033fa4b0b1
34 changed files with 590 additions and 149 deletions
+15 -22
View File
@@ -141,8 +141,7 @@ describe('apply (plugin lifecycle)', () => {
})
it('connects, syncs tools under the namespace, and registers a notification handler', async () => {
apply(ctx, stdioConfig)
await sleep(50)
await apply(ctx, stdioConfig)
expect(mockConnect).toHaveBeenCalled()
expect(mockListTools).toHaveBeenCalled()
@@ -152,11 +151,10 @@ describe('apply (plugin lifecycle)', () => {
})
it('rejects a duplicate serverName at load and leaves the first instance intact', async () => {
apply(ctx, stdioConfig)
await sleep(50)
await apply(ctx, stdioConfig)
expect(ctx.tools.get('mcp__srv__remote')).toBeDefined()
expect(() => { apply(ctx, stdioConfig) }).toThrow(/serverName "srv" is already in use/)
expect(() => { void apply(ctx, stdioConfig) }).toThrow(/serverName "srv" is already in use/)
// First instance unaffected.
expect(ctx.tools.get('mcp__srv__remote')).toBeDefined()
})
@@ -165,8 +163,7 @@ describe('apply (plugin lifecycle)', () => {
const first = new Context()
await first.plugin(SystemPrompt)
await first.plugin(ToolRegistry)
apply(first, stdioConfig)
await sleep(50)
await apply(first, stdioConfig)
await first.fiber.dispose()
await sleep(50)
@@ -176,16 +173,17 @@ describe('apply (plugin lifecycle)', () => {
const second = new Context()
await second.plugin(SystemPrompt)
await second.plugin(ToolRegistry)
expect(() => { apply(second, stdioConfig) }).not.toThrow()
await expect(apply(second, stdioConfig)).resolves.toBeUndefined()
await second.fiber.dispose()
})
it('scopes serverName reservations per app root', async () => {
const other = await mountRegistry()
apply(ctx, stdioConfig)
const first = apply(ctx, stdioConfig)
// Same serverName on a DIFFERENT root is fine.
expect(() => { apply(other, stdioConfig) }).not.toThrow()
await sleep(50)
const second = apply(other, stdioConfig)
await Promise.all([first, second])
expect(ctx.tools.get('mcp__srv__remote')).toBeDefined()
expect(other.tools.get('mcp__srv__remote')).toBeDefined()
@@ -194,8 +192,7 @@ describe('apply (plugin lifecycle)', () => {
it('logs error and registers no tools when connect fails; dispose is a no-op', async () => {
mockConnect.mockRejectedValue(new Error('connection refused'))
apply(ctx, stdioConfig)
await sleep(50)
await apply(ctx, stdioConfig)
expect(mockListTools).not.toHaveBeenCalled()
expect(ctx.tools.get('mcp__srv__remote')).toBeUndefined()
@@ -208,8 +205,7 @@ describe('apply (plugin lifecycle)', () => {
})
it('re-syncs tools on ToolListChanged notification', async () => {
apply(ctx, stdioConfig)
await sleep(50)
await apply(ctx, stdioConfig)
expect(ctx.tools.get('mcp__srv__remote')).toBeDefined()
@@ -226,8 +222,7 @@ describe('apply (plugin lifecycle)', () => {
})
it('keeps the previous generation when a re-sync fails', async () => {
apply(ctx, stdioConfig)
await sleep(50)
await apply(ctx, stdioConfig)
expect(ctx.tools.get('mcp__srv__remote')).toBeDefined()
mockListTools.mockRejectedValue(new Error('flaky server'))
@@ -242,7 +237,7 @@ describe('apply (plugin lifecycle)', () => {
// Load through ctx.plugin so ONLY the plugin's fiber is disposed — the
// registry must survive to observe the unregistration.
const fiber = ctx.plugin({ name: 'mcp-client', inject: ['tools'], apply }, stdioConfig)
await sleep(50)
await fiber
// Advance to a second generation first.
mockListTools.mockResolvedValue({
@@ -264,8 +259,7 @@ describe('apply (plugin lifecycle)', () => {
it('effect disposer handles client.close failure gracefully', async () => {
mockClose.mockRejectedValue(new Error('already closed'))
apply(ctx, stdioConfig)
await sleep(50)
await apply(ctx, stdioConfig)
// Should not throw when dispose is triggered.
await ctx.fiber.dispose()
@@ -283,8 +277,7 @@ describe('apply (plugin lifecycle)', () => {
toolCallTimeoutMs: 30_000,
}
apply(ctx, httpConfig)
await sleep(50)
await apply(ctx, httpConfig)
expect(mockConnect).toHaveBeenCalled()
expect(ctx.tools.get('mcp__web__remote')).toBeDefined()
@@ -43,21 +43,6 @@ async function mountRegistry(): Promise<Context> {
return ctx
}
/** Apply the MCP client plugin and wait for tools to be registered. */
async function applyAndWait(ctx: Context, config: Config, timeoutMs = 20_000): Promise<void> {
// Annotated bindings (not withResolvers<void>()): the tests lint layer runs
// no-invalid-void-type with default options, which rejects the explicit
// type argument in call position but accepts the inferred form.
const gate: PromiseWithResolvers<void> = Promise.withResolvers()
const timer = setTimeout(
() => { gate.reject(new Error(`applyAndWait timed out after ${timeoutMs}ms — no tools/change event`)) },
timeoutMs,
)
ctx.on('tools/change', () => { clearTimeout(timer); gate.resolve() })
apply(ctx, config)
await gate.promise
}
function sleep(ms: number): Promise<void> {
const gate: PromiseWithResolvers<void> = Promise.withResolvers()
setTimeout(gate.resolve, ms)
@@ -94,7 +79,7 @@ describe('fixture server — controlled scenarios', () => {
beforeAll(async () => {
ctx = await mountRegistry()
await applyAndWait(ctx, fixtureConfig)
await apply(ctx, fixtureConfig)
}, 30_000)
afterAll(async () => {
@@ -180,9 +165,9 @@ describe('fixture server — duplicate serverName', () => {
cwd: packageDir,
toolCallTimeoutMs: 15_000,
}
await applyAndWait(ctx, config)
await apply(ctx, config)
expect(() => { apply(ctx, config) }).toThrow(/serverName "dup" is already in use/)
expect(() => { void apply(ctx, config) }).toThrow(/serverName "dup" is already in use/)
await ctx.fiber.dispose()
await sleep(200)
@@ -192,7 +177,7 @@ describe('fixture server — duplicate serverName', () => {
describe('fixture server — disposal', () => {
it('disposes cleanly without error', async () => {
const ctx = await mountRegistry()
await applyAndWait(ctx, {
await apply(ctx, {
transport: 'stdio',
serverName: 'fixture',
command: process.execPath,
@@ -229,7 +214,7 @@ describe('server-everything — official test server', () => {
beforeAll(async () => {
ctx = await mountRegistry()
await applyAndWait(ctx, config)
await apply(ctx, config)
}, 60_000)
afterAll(async () => {
@@ -293,7 +278,7 @@ describe('server-filesystem — real filesystem operations', () => {
cwd: '',
toolCallTimeoutMs: 30_000,
}
await applyAndWait(ctx, config)
await apply(ctx, config)
}, 60_000)
afterAll(async () => {
@@ -409,7 +394,7 @@ describe('streamable-http — in-process MCP server', () => {
headers: { Authorization: 'Bearer e2e-test-token' },
toolCallTimeoutMs: 15_000,
}
await applyAndWait(ctx, config)
await apply(ctx, config)
}, 30_000)
afterAll(async () => {