Download tests/runtime/lifecycle.test.mjs from hunian003/message: direct link, hf CLI and curl.
- Browser
- Download file 16.4 kB
-
https://huggingface.co/spaces/hunian003/message/resolve/main/tests/runtime/lifecycle.test.mjs
- Command line
-
hf download hf://spaces/hunian003/message/tests/runtime/lifecycle.test.mjs
-
curl -L -o lifecycle.test.mjs https://huggingface.co/spaces/hunian003/message/resolve/main/tests/runtime/lifecycle.test.mjs
16.4 kB
| import test from 'node:test'; | |
| import assert from 'node:assert/strict'; | |
| import { createHash } from 'node:crypto'; | |
| import { mkdtemp, mkdir, readFile, writeFile, access } from 'node:fs/promises'; | |
| import { tmpdir } from 'node:os'; | |
| import { join } from 'node:path'; | |
| import { fileURLToPath } from 'node:url'; | |
| import { promisify } from 'node:util'; | |
| import { execFile, spawn } from 'node:child_process'; | |
| import { once } from 'node:events'; | |
| import { createInterface } from 'node:readline'; | |
| import { Client } from '@modelcontextprotocol/sdk/client/index.js'; | |
| import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'; | |
| import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'; | |
| const exec = promisify(execFile); | |
| const root = fileURLToPath(new URL('../../', import.meta.url)); | |
| const cli = join(root, 'apps/cloud/src/cli.mjs'); | |
| const temp = await mkdtemp(join(tmpdir(), 'message-lifecycle-')); | |
| const env = { ...process.env, npm_config_cache: join(temp, 'npm-cache') }; | |
| async function command(...args) { | |
| const { stdout } = await exec(process.execPath, [cli, ...args], { cwd: root, env, timeout: 90000 }); | |
| return JSON.parse(stdout); | |
| } | |
| async function pack(directory) { | |
| const { stdout } = await exec('npm', ['pack', directory, '--ignore-scripts', '--pack-destination', temp, '--json'], { cwd: root, env, timeout: 30000 }); | |
| return join(temp, JSON.parse(stdout)[0].filename); | |
| } | |
| async function hashHost() { | |
| const hash = createHash('sha256'); | |
| for (const file of ['package-lock.json', 'packages/runtime/package.json', 'packages/runtime/src/index.mjs', 'packages/runtime/src/redaction.mjs', 'apps/cloud/package.json', 'apps/cloud/src/cli.mjs']) hash.update(await readFile(join(root, file))); | |
| return hash.digest('hex'); | |
| } | |
| async function mcp(profile, action) { | |
| const client = new Client({ name: 'message-acceptance', version: '1.0.0' }); | |
| const transport = new StdioClientTransport({ command: process.execPath, args: [cli, 'mcp', profile], cwd: root, env, stderr: 'pipe' }); | |
| let stderr = ''; | |
| transport.stderr?.on('data', chunk => { stderr += chunk; }); | |
| try { | |
| await client.connect(transport); | |
| return await action(client); | |
| } catch (error) { error.message += `\nMCP stderr: ${stderr}`; throw error; } | |
| finally { await client.close(); } | |
| } | |
| async function httpMcp(profile, action) { | |
| const child = spawn(process.execPath, [cli, 'http', profile], { cwd: root, env, stdio: ['ignore', 'pipe', 'pipe'] }); | |
| const exited = once(child, 'exit'); | |
| const lines = createInterface({ input: child.stderr }); | |
| const client = new Client({ name: 'message-http-acceptance', version: '1.0.0' }); | |
| try { | |
| const url = await new Promise((resolve, reject) => { | |
| const timer = setTimeout(() => reject(new Error('HTTP MCP 启动超时')), 15000); | |
| child.once('exit', code => { clearTimeout(timer); reject(new Error(`HTTP MCP 提前退出: ${code}`)); }); | |
| lines.on('line', line => { | |
| try { | |
| const endpoint = JSON.parse(line); | |
| if (endpoint.transport === 'http' && endpoint.url) { clearTimeout(timer); resolve(endpoint.url); } | |
| } catch { /* 启动诊断不当作地址 */ } | |
| }); | |
| }); | |
| const transport = new StreamableHTTPClientTransport(new URL(url)); | |
| await client.connect(transport); | |
| await action(client, url); | |
| await transport.terminateSession(); | |
| } finally { | |
| await client.close(); | |
| lines.close(); | |
| child.kill('SIGTERM'); | |
| const timer = setTimeout(() => child.kill('SIGKILL'), 5000); | |
| await exited; | |
| clearTimeout(timer); | |
| } | |
| } | |
| test('独立安装、重启、MCP保护、禁用、升级和profile隔离', { timeout: 240000 }, async () => { | |
| const profile = join(temp, 'profile'); | |
| const other = join(temp, 'other'); | |
| await command('init', profile); | |
| await command('init', other); | |
| const before = await hashHost(); | |
| // 消费者已经就绪后才制作独立插件,不依赖消费者源码导入或宿主构建。 | |
| const fixture = join(temp, 'external-plugin'); | |
| await mkdir(fixture); | |
| const manifest = { name: '@message/acceptance-external', version: '1.0.0', type: 'module', exports: './index.mjs', files: ['index.mjs', 'cordis.patch.yml'], peerDependencies: { '@deepseek-ai/dsh-tools': '0.2.0-rc.2', '@deepseek-ai/cordis': '4.0.4' }, dsh: { bundle: { patch: './cordis.patch.yml' } } }; | |
| const source = version => `import { defineTool } from '@deepseek-ai/dsh-tools'; | |
| export const inject=['tools']; | |
| export function apply(ctx) { | |
| for (const name of ['acceptance_version','acceptance_denied','acceptance_redacted']) ctx.tools.register(defineTool({name,description:'独立验收工具',parameters:{},output:{schema:{type:'string'},render:(_a,v)=>[{type:'text',text:v}]},async execute(){return '${version}';}})); | |
| ctx.tools.guard(exec => exec.name === 'acceptance_denied' ? '验收保护拒绝' : undefined); | |
| ctx.on('tools/post-execute',async(exec,result,next)=>exec.name === 'acceptance_redacted' ? { kind:'accept', content:[{type:'text',text:'已脱敏'}] } : next()); | |
| }`; | |
| await writeFile(join(fixture, 'package.json'), JSON.stringify(manifest)); | |
| await writeFile(join(fixture, 'cordis.patch.yml'), '- insert:\n - id: acceptance-external\n name: "@message/acceptance-external"\n'); | |
| await writeFile(join(fixture, 'index.mjs'), source('v1')); | |
| for (const directory of [join(root, 'plugins/text-tools'), join(root, 'plugins/mcp-export'), fixture]) { | |
| const result = await command('install', profile, await pack(directory)); | |
| assert.equal(result.application, 'restart-required'); | |
| assert.equal(result.packageResult.exitCode, 0); | |
| } | |
| assert.equal((await command('list', other)).tools.length, 0); | |
| await mcp(profile, async client => { | |
| const list = await client.listTools(); | |
| assert(list.tools.some(x => x.name === 'message_text_uppercase')); | |
| assert(list.tools.some(x => x.name === 'acceptance_version')); | |
| const called = await client.callTool({ name: 'message_text_uppercase', arguments: { text: 'hello dsh' } }); | |
| assert.deepEqual(called.content, [{ type: 'text', text: 'HELLO DSH' }]); | |
| assert.equal((await client.callTool({ name: 'acceptance_version' })).content[0].text, 'v1'); | |
| assert.equal((await client.callTool({ name: 'acceptance_denied' })).isError, true); | |
| assert.equal((await client.callTool({ name: 'message_text_uppercase', arguments: { text: 7 } })).isError, true); | |
| const redacted = await client.callTool({ name: 'acceptance_redacted' }); | |
| assert.equal(redacted.content[0].text, '已脱敏'); | |
| assert.equal(redacted.structuredContent, undefined); | |
| }); | |
| await httpMcp(profile, async (client, url) => { | |
| assert.equal(new URL(url).hostname, '127.0.0.1'); | |
| await assert.rejects(command('remove', profile, '@message/acceptance-external'), /profile正在运行/); | |
| await assert.rejects(command('install', profile, join(temp, 'message-text-tools-0.1.0.tgz')), /profile正在运行/); | |
| assert((await client.listTools()).tools.some(x => x.name === 'acceptance_version')); | |
| assert.equal((await client.callTool({ name: 'acceptance_version' })).content[0].text, 'v1'); | |
| assert.equal((await client.callTool({ name: 'acceptance_denied' })).isError, true); | |
| const rejected = await fetch(url, { method: 'POST', headers: { origin: 'https://untrusted.example', 'content-type': 'application/json' }, body: '{}' }); | |
| assert.equal(rejected.status, 403); | |
| }); | |
| const disabled = await command('disable', profile, '@message/acceptance-external'); | |
| assert.equal(disabled.application, 'restart-required'); | |
| await mcp(profile, async client => { | |
| assert(!(await client.listTools()).tools.some(x => x.name === 'acceptance_version')); | |
| assert.equal((await client.callTool({ name: 'acceptance_version' })).isError, true); | |
| }); | |
| await command('enable', profile, '@message/acceptance-external'); | |
| manifest.version = '1.0.1'; | |
| await writeFile(join(fixture, 'package.json'), JSON.stringify(manifest)); | |
| await writeFile(join(fixture, 'index.mjs'), source('v2')); | |
| const upgraded = await command('install', profile, await pack(fixture)); | |
| assert.equal(upgraded.application, 'restart-required'); | |
| await mcp(profile, async client => assert.equal((await client.callTool({ name: 'acceptance_version' })).content[0].text, 'v2')); | |
| const removed = await command('remove', profile, '@message/acceptance-external'); | |
| assert.equal(removed.application, 'restart-required'); | |
| assert(!(await command('list', profile)).bundles.some(x => x.name === '@message/acceptance-external')); | |
| assert.equal(await hashHost(), before, '安装与升级不得改动消费者文件'); | |
| console.log(`消费者文件 SHA256 未变: ${before}`); | |
| }); | |
| test('禁止使用仓库模板作为可写profile', async () => { | |
| await assert.rejects(command('init', join(root, 'profiles/minimal')), /profile 必须位于应用仓库之外/); | |
| }); | |
| test('保留已有根配置,专用根手改则拒绝覆盖', async () => { | |
| const profile = join(temp, 'preserve-root'); | |
| await command('init', profile); | |
| const custom = '# 用户原有配置\n- name: custom-plugin\n'; | |
| await writeFile(join(profile, 'cordis.yml'), custom); | |
| await command('list', profile); | |
| assert.equal(await readFile(join(profile, 'cordis.yml'), 'utf8'), custom); | |
| await writeFile(join(profile, '.message-root.yml'), custom); | |
| await assert.rejects(command('list', profile), /不是空根/); | |
| assert.equal(await readFile(join(profile, '.message-root.yml'), 'utf8'), custom); | |
| }); | |
| test('坏bundle拒绝执行但可以经管理命令禁用恢复', async () => { | |
| const profile = join(temp, 'broken-bundle'); | |
| await command('init', profile); | |
| const path = join(profile, 'package.json'); | |
| const manifest = JSON.parse(await readFile(path, 'utf8')); | |
| manifest.dsh.profile.bundles = ['@message/missing-bundle']; | |
| await writeFile(path, JSON.stringify(manifest)); | |
| await assert.rejects(command('call', profile, 'anything'), /missing-bundle/); | |
| const result = await command('disable', profile, '@message/missing-bundle'); | |
| assert.equal(result.application, 'restart-required'); | |
| assert.equal((await command('list', profile)).tools.length, 0); | |
| }); | |
| test('安装中SIGTERM经官方manager取消并恢复manifest', { timeout: 60000 }, async () => { | |
| const profile = join(temp, 'cancel-install'); | |
| await command('init', profile); | |
| const before = await readFile(join(profile, 'package.json'), 'utf8'); | |
| const archive = await pack(join(root, 'plugins/mcp-export')); | |
| const child = spawn(process.execPath, [cli, 'install', profile, archive], { cwd: root, env, stdio: ['ignore', 'pipe', 'pipe'] }); | |
| let stdout = '', stderr = ''; | |
| child.stdout.on('data', chunk => { stdout += chunk; }); | |
| child.stderr.on('data', chunk => { stderr += chunk; }); | |
| const exited = once(child, 'close'); | |
| let installing = false; | |
| for (let i=0; i<200; i++) { | |
| try { await access(join(profile, '.plugin-manager/run.json')); installing = true; break; } catch {} | |
| if (child.exitCode !== null) break; | |
| await new Promise(resolve => setTimeout(resolve, 25)); | |
| } | |
| assert(installing, `未观察到真实包管理进程: ${stdout} ${stderr}`); | |
| child.kill('SIGTERM'); | |
| const timer = setTimeout(() => child.kill('SIGKILL'), 25000); | |
| const outcome = await exited; clearTimeout(timer); | |
| assert(stdout, `取消未返回JSON outcome=${JSON.stringify(outcome)} stderr=${stderr}`); | |
| assert.equal(JSON.parse(stdout).application, 'cancelled', stderr); | |
| assert.equal(await readFile(join(profile, 'package.json'), 'utf8'), before); | |
| await assert.rejects(access(join(profile, '.plugin-manager/run.json'))); | |
| }); | |
| test('JSON配置保存、重启生效与错误回滚', { timeout: 60000 }, async () => { | |
| const profile = join(temp, 'configure'); | |
| await command('init', profile); | |
| await command('install', profile, await pack(join(root, 'plugins/text-tools'))); | |
| const row = (await command('list', profile)).plugins.find(item => item.moduleName === '@message/text-tools'); | |
| assert.deepEqual(row.config, {}); | |
| const result = await command('configure', profile, row.entry_id, JSON.stringify({ prefix: '配置:' })); | |
| assert.equal(result.application, 'restart-required'); | |
| const value = await command('call', profile, 'message_text_uppercase', JSON.stringify({ text: 'hello' })); | |
| assert.equal(value.content[0].text, '配置:HELLO'); | |
| const before = await readFile(join(profile, 'cordis.patch.yml'), 'utf8'); | |
| await assert.rejects(command('configure', profile, row.entry_id, JSON.stringify({ prefix: 5 }))); | |
| assert.equal(await readFile(join(profile, 'cordis.patch.yml'), 'utf8'), before); | |
| await assert.rejects(command('configure', profile, row.entry_id, JSON.stringify({ api_key: 'test-only-not-a-real-key' })), /凭据/); | |
| await assert.rejects(command('configure', profile, 'include:message-tools', '{}'), /非系统/); | |
| }); | |
| test('环境引用只在worker解析且输出不泄露真实值', { timeout: 60000 }, async () => { | |
| const profile = join(temp, 'environment-reference'); | |
| const secret = 'fixture-only-not-a-real-service-credential'; | |
| const allowed = { ...env, MESSAGE_PLUGIN_ENV_ALLOWLIST: '["SERVICE_FIXTURE"]' }; | |
| const invoke = async (extra, ...args) => { | |
| const { stdout } = await exec(process.execPath, [cli, ...args], { cwd: root, env: { ...allowed, ...extra }, timeout: 30000 }); | |
| assert(!stdout.includes(secret)); return JSON.parse(stdout); | |
| }; | |
| await command('init', profile); | |
| await command('install', profile, await pack(join(root, 'plugins/text-tools'))); | |
| await invoke({}, 'configure', profile, 'include:message-text-tools', '{"prefix":{"$env":"SERVICE_FIXTURE"}}'); | |
| const row = (await invoke({}, 'list', profile)).plugins.find(item => item.row_id === 'message-text-tools'); | |
| assert.deepEqual(row.config, { prefix: { $env: 'SERVICE_FIXTURE' } }); | |
| await assert.rejects(invoke({}, 'call', profile, 'message_text_uppercase', '{"text":"missing"}'), /缺少本profile/); | |
| const result = await invoke({ SERVICE_FIXTURE: secret }, 'call', profile, 'message_text_uppercase', '{"text":"hello"}'); | |
| assert.equal(result.content[0].text, '[REDACTED]HELLO'); | |
| assert.equal(result.value, '[REDACTED]HELLO'); | |
| await assert.rejects(invoke({}, 'configure', profile, 'include:message-text-tools', '{"prefix":{"$env":"OTHER_PROFILE_SECRET"}}'), /允许名单/); | |
| await assert.rejects(invoke({}, 'configure', profile, 'include:message-text-tools', '{"prefix":{"__jsExpr":"process.env.SERVICE_FIXTURE"}}'), /表达式/); | |
| }); | |
| test('静态管理与配置不挂载插件资源', { timeout: 60000 }, async () => { | |
| const profile = join(temp, 'static-management'); | |
| const fixture = join(temp, 'resource-plugin'); | |
| const marker = join(temp, 'resource-mounts'); | |
| await mkdir(fixture); | |
| await writeFile(join(fixture, 'package.json'), JSON.stringify({ name: '@message/resource-fixture', version: '1.0.0', type: 'module', exports: './index.mjs', files: ['index.mjs', 'cordis.patch.yml'], dependencies: { '@deepseek-ai/schemastery': '3.18.4' }, peerDependencies: { '@deepseek-ai/dsh-tools': '0.2.0-rc.2', '@deepseek-ai/cordis': '4.0.4' }, dsh: { bundle: { patch: './cordis.patch.yml' } } })); | |
| await writeFile(join(fixture, 'cordis.patch.yml'), '- insert:\n - id: resource-fixture\n name: "@message/resource-fixture"\n'); | |
| await writeFile(join(fixture, 'index.mjs'), `import {appendFileSync} from 'node:fs';import z from '@deepseek-ai/schemastery';import {defineTool} from '@deepseek-ai/dsh-tools';export const inject=['tools'];export const Config=z.object({prefix:z.string().default('')});export function apply(ctx,c){appendFileSync(${JSON.stringify(marker)},'mount\\n');ctx.tools.register(defineTool({name:'resource_tool',description:'resource fixture',parameters:{},output:{schema:{type:'string'},render:(_a,v)=>[{type:'text',text:v}]},async execute(){return c.prefix+'ok';}}));}`); | |
| await command('init', profile); | |
| await command('install', profile, await pack(fixture)); | |
| await command('list', profile); | |
| await command('configure', profile, 'include:resource-fixture', '{"prefix":"configured:"}'); | |
| await command('disable', profile, '@message/resource-fixture'); | |
| await command('enable', profile, '@message/resource-fixture'); | |
| await assert.rejects(access(marker)); | |
| assert.equal((await command('call', profile, 'resource_tool')).content[0].text, 'configured:ok'); | |
| assert.equal(await readFile(marker, 'utf8'), 'mount\n'); | |
| await command('list', profile); | |
| assert.equal(await readFile(marker, 'utf8'), 'mount\n'); | |
| }); | |