Files
RuoYi-Vue3-FastAPI/ruoyi-fastapi-frontend/tests/plugins/pluginBridgeCompatibility.test.js
T
insistence 4bd83bc7cd feat: 支持 v2 插件、独立前端和签名制品发布 (#130)
* feat: 支持 v2 插件、原生扩展与签名发布

* fix: 修复测试

* feat: 增强宿主 SDK、运行观测与制品维护能力

* feat: 增强文件传输、配置观测与本地开发能力

* feat: 支持 SSE 实时事件流并完善开发脚手架

* feat: 完善长连接鉴权复核与关闭回收

* feat: 增强生命周期与任务关闭可靠性

* feat: 收紧健康检查契约并补齐前端与真实网络验收

* feat: 增加SDK溯源与离线更新工具并验证跨版本兼容

* feat: 增加持久化业务示例与受控 Python 交付

* feat: 增加统一运行诊断与多worker状态观测

* feat: 增加固定目标发布等待与自动验收

* fix: 修复多环境插件部署前缀与代理配置不一致

* fix: 修复测试
2026-10-09 23:49:45 +08:00

227 lines
7.8 KiB
JavaScript

import assert from 'node:assert/strict'
import { createHash } from 'node:crypto'
import { readFileSync } from 'node:fs'
import {
createPluginClient,
createPluginHostBridge,
PLUGIN_BRIDGE_SDK_VERSION,
PLUGIN_BRIDGE_VERSION,
validatePluginSession,
} from '../../src/utils/pluginBridge.js'
import * as legacySdk from './fixtures/bridge-b0d16a3e/pluginBridge.js'
import { createJsonOnlyHost } from './fixtures/json-only-host.js'
const ORIGIN = 'https://compatibility.test'
const metadata = JSON.parse(
readFileSync(new URL('../../src/utils/pluginBridge.sdk.json', import.meta.url))
)
const declarations = readFileSync(
new URL('../../src/utils/pluginBridge.d.ts', import.meta.url),
'utf8'
)
const sourceCommit = 'b0d16a3eae1979afabb838245c00e1f816aa59e9'
const legacyRoot = new URL('./fixtures/bridge-b0d16a3e/', import.meta.url)
const provenance = JSON.parse(readFileSync(new URL('provenance.json', legacyRoot)))
// 完整快照保留提交中的原始字节,由目录内 .gitattributes 禁止自动换行转换。
assert.equal(provenance.sourceCommit, sourceCommit)
assert.equal(provenance.sourcePath, 'ruoyi-fastapi-frontend/src/utils/pluginBridge.js')
assert.equal(provenance.sdkVersion, null, '该历史快照没有独立 SDK 版本号')
assert.equal(provenance.bridgeVersion, 1)
assert.equal(
createHash('sha256')
.update(readFileSync(new URL('pluginBridge.js', legacyRoot)))
.digest('hex'),
provenance.sha256
)
assert.equal(Object.hasOwn(legacySdk, 'PLUGIN_BRIDGE_SDK_VERSION'), false)
assert.deepEqual(metadata, {
schemaVersion: 1,
sdkVersion: PLUGIN_BRIDGE_SDK_VERSION,
bridgeVersion: PLUGIN_BRIDGE_VERSION,
capabilities: { files: 1, streams: 1 },
})
assert.equal(PLUGIN_BRIDGE_SDK_VERSION, '1.0.0')
assert.equal(PLUGIN_BRIDGE_VERSION, 1, '源码交付版本不能改变 bridge v1 消息协议')
assert.ok(declarations.includes(`export const PLUGIN_BRIDGE_SDK_VERSION: '${metadata.sdkVersion}'`))
assert.ok(declarations.includes(`export const PLUGIN_BRIDGE_VERSION: ${metadata.bridgeVersion}`))
function windowPair() {
const makeWindow = () => {
const listeners = new Set()
return {
messages: [],
listeners,
addEventListener(type, listener) {
if (type === 'message') listeners.add(listener)
},
removeEventListener(type, listener) {
if (type === 'message') listeners.delete(listener)
},
dispatch(event) {
for (const listener of [...listeners]) listener(event)
},
}
}
const hostWindow = makeWindow()
const childWindow = makeWindow()
for (const [target, source] of [
[hostWindow, childWindow],
[childWindow, hostWindow],
]) {
target.postMessage = (value, targetOrigin) => {
assert.equal(targetOrigin, ORIGIN)
const data = structuredClone(value)
target.messages.push(data)
queueMicrotask(() => target.dispatch({ data, source, origin: ORIGIN }))
}
}
return { hostWindow, childWindow }
}
function clientOptions({ hostWindow, childWindow }) {
return {
pluginId: 'demo',
eventTarget: childWindow,
parentWindow: hostWindow,
origin: ORIGIN,
timeoutMs: 1000,
}
}
// 历史客户端使用固定源码;宿主使用当前源码,实际交换 JSON、Blob 与逐条 SSE 消息。
{
const pair = windowPair()
const requests = []
const streams = []
const session = validatePluginSession(
{
pluginId: 'demo',
bridgeVersion: 1,
uiBase: '/apps/demo/ui/',
apiBase: '/apps/demo/api/',
csrfToken: 'compatibility-csrf-only',
expiresIn: 300,
},
'demo'
)
const host = createPluginHostBridge({
pluginId: 'demo',
session,
getTarget: () => pair.childWindow,
eventTarget: pair.hostWindow,
origin: ORIGIN,
request: async (config) => {
requests.push(config)
if (config.responseType === 'blob') return new Blob(['legacy download'])
if (config.data instanceof FormData) {
assert.equal(await config.data.get('file').text(), 'legacy upload')
assert.equal(config.data.get('category'), 'compatibility')
return { received: config.data.get('file').size }
}
return { code: 200, data: config.data }
},
stream: async function* (config) {
streams.push(config)
yield { event: 'tick', data: 'first', id: '5' }
yield { event: 'tick', data: 'second', id: '6' }
},
})
const client = legacySdk.createPluginClient(clientOptions(pair))
try {
const context = await client.ready
assert.equal(context.capabilities.files.version, metadata.capabilities.files)
assert.equal(context.capabilities.streams.version, metadata.capabilities.streams)
assert.deepEqual(
await client.request({ method: 'POST', path: 'echo', data: { value: 'legacy' } }),
{
code: 200,
data: { value: 'legacy' },
}
)
assert.deepEqual(
await client.upload({
path: 'files/upload',
file: new Blob(['legacy upload']),
fields: { category: 'compatibility' },
}),
{ received: 13 }
)
assert.equal(await (await client.download({ path: 'files/report' })).text(), 'legacy download')
const events = []
await client.stream(
{ path: 'events', params: { count: 6 }, lastEventId: '4' },
{
async onEvent(event) {
await Promise.resolve()
events.push(event)
},
}
)
assert.deepEqual(events, [
{ event: 'tick', data: 'first', id: '5' },
{ event: 'tick', data: 'second', id: '6' },
])
assert.deepEqual(
requests.map((config) => config.url),
['/apps/demo/api/echo', '/apps/demo/api/files/upload', '/apps/demo/api/files/report']
)
for (const config of requests) {
assert.equal(config.headers.isToken, false)
assert.equal(config.headers.Authorization, undefined)
assert.equal(config.headers['X-Plugin-CSRF'], session.csrfToken)
}
assert.equal(streams.length, 1)
assert.equal(streams[0].url, '/apps/demo/api/events')
assert.deepEqual(streams[0].params, { count: 6 })
assert.equal(streams[0].lastEventId, '4')
assert.equal(streams[0].csrfToken, session.csrfToken)
for (const message of [...pair.hostWindow.messages, ...pair.childWindow.messages]) {
assert.equal(message.version, 1)
assert.equal(message.sdkVersion, undefined)
assert.equal(message.payload?.csrfToken, undefined)
}
} finally {
client.destroy()
host.destroy()
}
assert.equal(pair.hostWindow.listeners.size, 0)
assert.equal(pair.childWindow.listeners.size, 0)
}
// 旧 JSON 协议夹具不公布新增能力;未知版本也必须在客户端拒绝,而非先发送再失败。
for (const capabilities of [
undefined,
{ files: { version: 2 }, streams: { version: 2 } },
{ files: { version: '1' }, streams: { version: '1' } },
]) {
const pair = windowPair()
const host = createJsonOnlyHost({ ...pair, origin: ORIGIN, capabilities })
const client = createPluginClient(clientOptions(pair))
try {
await client.ready
const payload = { method: 'GET', path: 'info', params: { page: 1 } }
assert.deepEqual(await client.request(payload), { accepted: payload })
await assert.rejects(
client.upload({ path: 'files/upload', file: new Blob(['test']) }),
/不支持文件传输/
)
await assert.rejects(client.download({ path: 'files/report' }), /不支持文件传输/)
await assert.rejects(client.stream({ path: 'events' }, { onEvent() {} }), /不支持实时事件流/)
assert.deepEqual(await client.request(payload), { accepted: payload })
assert.deepEqual(
host.calls.map((message) => message.type),
['request', 'request']
)
assert.deepEqual(
pair.hostWindow.messages.map((message) => message.type),
['ready', 'request', 'request']
)
} finally {
client.destroy()
host.destroy()
}
assert.equal(pair.hostWindow.listeners.size, 0)
assert.equal(pair.childWindow.listeners.size, 0)
}