feat: vibe-registry M1 — 万物皆注册框架骨架
- core: registerLayout/Widget/Action(泛型)/Schema + 校验闸门 - definePage: 页面只管业务,结构来自注册表 - presets: admin / app-shell 预设布局(vr add 物化) - cli: vr list/add/update 防覆盖 - examples/qa-orchestrator: 实战编排验证(workspace:* 依赖)
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "qa-orchestrator",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"description": "vibe-registry 实战编排:测试编排系统(多页面 + 自定义注册)",
|
||||
"scripts": {
|
||||
"typecheck": "tsc --noEmit",
|
||||
"orchestrate": "node --experimental-strip-types scripts/orchestrate.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"vibe-registry": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.8.0",
|
||||
"@types/node": "^22.0.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* 套件总览页(overview)
|
||||
* 业务:QA 看板上展示所有套件、总通过率、失败数
|
||||
* 布局:qa-console;页面只管业务数据
|
||||
*/
|
||||
import { definePage } from 'vibe-registry'
|
||||
|
||||
export const overviewPage = definePage({
|
||||
layout: 'qa-console',
|
||||
title: '测试编排 · 总览',
|
||||
blocks: [
|
||||
{ widget: 'suite-list', bind: { suites: 'suites' } },
|
||||
{
|
||||
widget: 'suite-detail-view',
|
||||
props: { mode: 'summary' },
|
||||
bind: { cases: 'summary' },
|
||||
},
|
||||
],
|
||||
setup: async () => {
|
||||
// 真实场景:从 API 拉取;这里用模拟数据演示编排
|
||||
const suites = [
|
||||
{ id: 'auth', name: '认证模块', passedRate: 0.92, lastResult: 'passed' },
|
||||
{ id: 'billing', name: '计费模块', passedRate: 0.71, lastResult: 'failed' },
|
||||
{ id: 'search', name: '搜索模块', passedRate: 1.0, lastResult: 'passed' },
|
||||
]
|
||||
const total = suites.length
|
||||
const failed = suites.filter((s) => s.lastResult === 'failed').length
|
||||
return {
|
||||
suites,
|
||||
summary: {
|
||||
total,
|
||||
failed,
|
||||
passingRate: Math.round((1 - failed / total) * 100),
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* 运行记录页(run history)
|
||||
* 业务:全部历史运行 + 失败率统计
|
||||
*/
|
||||
import { definePage } from 'vibe-registry'
|
||||
|
||||
export const runHistoryPage = definePage({
|
||||
layout: 'qa-console',
|
||||
title: '测试编排 · 运行记录',
|
||||
blocks: [
|
||||
{ widget: 'suite-list', bind: { suites: 'suites' } },
|
||||
{ widget: 'suite-detail-view', props: { mode: 'history' }, bind: { cases: 'runs' } },
|
||||
],
|
||||
setup: async () => {
|
||||
return {
|
||||
suites: [
|
||||
{ id: 'auth', name: '认证模块', passedRate: 0.92, lastResult: 'passed' },
|
||||
{ id: 'billing', name: '计费模块', passedRate: 0.71, lastResult: 'failed' },
|
||||
],
|
||||
runs: [
|
||||
{ id: 'r1', name: 'auth #0421', priority: 'P0', status: 'passed', durationMs: 980 },
|
||||
{ id: 'r2', name: 'auth #0420', priority: 'P1', status: 'skipped', durationMs: null },
|
||||
{ id: 'r3', name: 'billing #0419', priority: 'P0', status: 'failed', durationMs: 2100 },
|
||||
],
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* 套件详情页(suite detail)
|
||||
* 业务:展示单个套件的用例列表,提供重跑/停止动作
|
||||
* 注意:页面不内联任何"重跑逻辑",只声明引用 run-suite action —— 行为万物皆注册
|
||||
*/
|
||||
import { definePage } from 'vibe-registry'
|
||||
|
||||
export const suiteDetailPage = definePage({
|
||||
layout: 'qa-console',
|
||||
title: '测试编排 · 套件详情',
|
||||
blocks: [
|
||||
{ widget: 'suite-list', bind: { suites: 'suites' } },
|
||||
{
|
||||
widget: 'suite-detail-view',
|
||||
props: { mode: 'detail', suiteId: 'current' },
|
||||
bind: { cases: 'cases', suite: 'suite' },
|
||||
},
|
||||
],
|
||||
setup: async (ctx) => {
|
||||
const suiteId = ctx.params.id ?? 'auth'
|
||||
const cases = [
|
||||
{ id: 'a1', name: '登录成功', priority: 'P0', status: 'passed', durationMs: 120 },
|
||||
{ id: 'a2', name: '登录失败锁定', priority: 'P1', status: 'failed', durationMs: 340 },
|
||||
{ id: 'a3', name: '令牌过期刷新', priority: 'P1', status: 'running', durationMs: null },
|
||||
]
|
||||
return {
|
||||
suites: [
|
||||
{ id: 'auth', name: '认证模块', passedRate: 0.92, lastResult: 'passed' },
|
||||
{ id: 'billing', name: '计费模块', passedRate: 0.71, lastResult: 'failed' },
|
||||
],
|
||||
suite: { id: suiteId, name: `${suiteId} 套件` },
|
||||
cases,
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
rerun: async (payload: { suiteId?: string }) => {
|
||||
// 页面只做"把用户意图转成 action 调用",实现仍在注册表
|
||||
const { runSuiteAction } = await import('../registry/actions/run-suite.ts')
|
||||
return runSuiteAction.handler({ suiteId: payload.suiteId ?? 'auth' }, null)
|
||||
},
|
||||
},
|
||||
})
|
||||
Generated
+48
@@ -0,0 +1,48 @@
|
||||
lockfileVersion: '9.0'
|
||||
|
||||
settings:
|
||||
autoInstallPeers: true
|
||||
excludeLinksFromLockfile: false
|
||||
|
||||
importers:
|
||||
|
||||
.:
|
||||
dependencies:
|
||||
vibe-registry:
|
||||
specifier: file:../..
|
||||
version: file:../..
|
||||
devDependencies:
|
||||
'@types/node':
|
||||
specifier: ^22.0.0
|
||||
version: 22.20.1
|
||||
typescript:
|
||||
specifier: ^5.8.0
|
||||
version: 5.9.3
|
||||
|
||||
packages:
|
||||
|
||||
'@types/node@22.20.1':
|
||||
resolution: {integrity: sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==}
|
||||
|
||||
typescript@5.9.3:
|
||||
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
|
||||
engines: {node: '>=14.17'}
|
||||
hasBin: true
|
||||
|
||||
undici-types@6.21.0:
|
||||
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
|
||||
|
||||
vibe-registry@file:../..:
|
||||
resolution: {directory: ../.., type: directory}
|
||||
|
||||
snapshots:
|
||||
|
||||
'@types/node@22.20.1':
|
||||
dependencies:
|
||||
undici-types: 6.21.0
|
||||
|
||||
typescript@5.9.3: {}
|
||||
|
||||
undici-types@6.21.0: {}
|
||||
|
||||
vibe-registry@file:../..: {}
|
||||
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* run-suite —— 重跑测试套件 action
|
||||
* 行为注册:页面的"重跑"按钮只声明引用该 action,不内联逻辑
|
||||
*/
|
||||
import { registerAction } from 'vibe-registry'
|
||||
|
||||
export const runSuiteAction = registerAction({
|
||||
kind: 'action',
|
||||
id: 'run-suite',
|
||||
description: '重跑指定测试套件(全量)',
|
||||
handler: async (payload: { suiteId: string }, _ctx) => {
|
||||
// M1:真实执行器后续接入;这里返回执行意图供编排层消费
|
||||
return { started: true, suiteId: payload.suiteId, at: Date.now() }
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* stop-run —— 停止运行中的套件 action
|
||||
*/
|
||||
import { registerAction } from 'vibe-registry'
|
||||
|
||||
export const stopRunAction = registerAction({
|
||||
kind: 'action',
|
||||
id: 'stop-run',
|
||||
description: '停止正在运行的测试套件',
|
||||
handler: async (payload: { runId: string }, _ctx) => {
|
||||
return { stopped: true, runId: payload.runId, at: Date.now() }
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* 资产集中注册入口
|
||||
* 模拟脚手架/自动化生成:所有 registry/** 在此汇聚(副作用 import)
|
||||
* 页面只需 import 本文件即可让全部注册生效
|
||||
*/
|
||||
import './layouts/qa-console.ts'
|
||||
import './widgets/suite-list.ts'
|
||||
import './widgets/suite-detail-view.ts'
|
||||
import './widgets/status-badge.ts'
|
||||
import './schemas/test-case-fields.ts'
|
||||
import './actions/run-suite.ts'
|
||||
import './actions/stop-run.ts'
|
||||
|
||||
// 注册完成后,导出注册表供编排层校验
|
||||
import { registry } from 'vibe-registry'
|
||||
export { registry }
|
||||
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* qa-console —— 测试编排控制台布局(自定义布局,非预设)
|
||||
* 业务场景:QA 负责人在侧栏切换套件,主区查看运行结果
|
||||
*/
|
||||
import { registerLayout } from 'vibe-registry'
|
||||
|
||||
export const qaConsoleLayout = registerLayout({
|
||||
kind: 'layout',
|
||||
id: 'qa-console',
|
||||
slots: ['suite-sidebar', 'main-content'],
|
||||
description: '测试编排控制台:左侧套件列表 + 主内容区',
|
||||
preview: {
|
||||
title: 'QA Console · 测试编排控制台',
|
||||
demoBlocks: [
|
||||
{ slot: 'suite-sidebar', widget: 'suite-list', props: { suites: 'suiteSummary' } },
|
||||
{ slot: 'main-content', widget: 'suite-detail-view' },
|
||||
],
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* test-case-fields —— 测试用例字段 schema
|
||||
* 用例表格/表单的列定义来源(万物皆注册:连列都是注册的)
|
||||
*/
|
||||
import { registerSchema } from 'vibe-registry'
|
||||
|
||||
export const testCaseFields = registerSchema({
|
||||
kind: 'schema',
|
||||
id: 'test-case-fields',
|
||||
description: '测试用例字段:名称 / 优先级 / 状态 / 耗时',
|
||||
fields: [
|
||||
{ key: 'name', label: '用例名' },
|
||||
{ key: 'priority', label: '优先级', type: 'P0|P1|P2' },
|
||||
{ key: 'status', label: '状态', type: 'passed|failed|running|skipped' },
|
||||
{ key: 'durationMs', label: '耗时(ms)', type: 'number' },
|
||||
],
|
||||
})
|
||||
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* status-badge —— 用例状态徽标 widget(通过/失败/运行中/跳过)
|
||||
*/
|
||||
import { registerWidget } from 'vibe-registry'
|
||||
|
||||
export const statusBadge = registerWidget({
|
||||
kind: 'widget',
|
||||
id: 'status-badge',
|
||||
description: '用例状态徽标:passed / failed / running / skipped',
|
||||
})
|
||||
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* suite-detail-view —— 套件详情 widget
|
||||
* 渲染套件用例列表 + 状态徽标,支持重跑/停止动作
|
||||
*/
|
||||
import { registerWidget } from 'vibe-registry'
|
||||
|
||||
export const suiteDetailView = registerWidget({
|
||||
kind: 'widget',
|
||||
id: 'suite-detail-view',
|
||||
schemaId: 'test-case-fields',
|
||||
description: '套件详情:用例表格 + 状态徽标 + 重跑/停止',
|
||||
})
|
||||
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* suite-list —— 测试套件列表 widget
|
||||
* 展示全部套件与通过率,点击进入套件详情
|
||||
*/
|
||||
import { registerWidget } from 'vibe-registry'
|
||||
|
||||
export const suiteListWidget = registerWidget({
|
||||
kind: 'widget',
|
||||
id: 'suite-list',
|
||||
description: '测试套件列表:名称 + 最近结果 + 通过率',
|
||||
})
|
||||
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* orchestrate —— 编排层验证脚本
|
||||
* 模拟"渲染内核消费前的编排":集中注册 → 校验 → 执行 setup 数据流 → 输出解析树
|
||||
*
|
||||
* 运行:node --experimental-strip-types scripts/orchestrate.ts
|
||||
*/
|
||||
import '../registry/index.ts' // ← 集中注册(副作用)
|
||||
import { registry } from 'vibe-registry'
|
||||
import { overviewPage } from '../pages/overview.ts'
|
||||
import { suiteDetailPage } from '../pages/suite-detail.ts'
|
||||
import { runHistoryPage } from '../pages/run-history.ts'
|
||||
|
||||
function dumpRegistry() {
|
||||
console.log('═══ 注册表(万物皆注册)═══')
|
||||
for (const e of registry.listAll()) {
|
||||
const preview = e.kind === 'layout' && e.preview ? ` 预览:「${e.preview.title}」` : ''
|
||||
console.log(` ${e.kind.padEnd(7)} ${e.id}${preview}`)
|
||||
}
|
||||
}
|
||||
|
||||
async function orchestrate() {
|
||||
console.log('═══ 编排层 · 解析树 ═══\n')
|
||||
const pages = [
|
||||
{ name: 'overview', page: overviewPage, ctx: { query: {}, params: {} } },
|
||||
{ name: 'suite-detail', page: suiteDetailPage, ctx: { query: {}, params: { id: 'billing' } } },
|
||||
{ name: 'run-history', page: runHistoryPage, ctx: { query: {}, params: {} } },
|
||||
]
|
||||
|
||||
for (const { name, page, ctx } of pages) {
|
||||
const layout = registry.store.layouts.get(page.layout)
|
||||
if (!layout) throw new Error(`页面 ${name} 引用了未注册布局 ${page.layout}(本不该发生,definePage 已校验)`)
|
||||
|
||||
// 业务层:页面只管 setup,数据喂给 blocks
|
||||
const data = page.setup ? await page.setup(ctx) : {}
|
||||
|
||||
console.log(`◆ 页面「${page.title ?? name}」`)
|
||||
console.log(` layout: ${page.layout} slots=[${layout.slots.join(', ')}]`)
|
||||
for (const b of page.blocks) {
|
||||
const widget = registry.store.widgets.get(b.widget)
|
||||
const bound = b.bind
|
||||
? Object.entries(b.bind)
|
||||
.map(([prop, key]) => `${prop}←${key}(${data[key] === undefined ? '?' : Array.isArray(data[key]) ? `${(data[key] as unknown[]).length}项` : typeof data[key]})`)
|
||||
.join(' ')
|
||||
: ''
|
||||
console.log(` └ widget: ${b.widget} props=${JSON.stringify(b.props ?? {})} bind=[${bound}]`)
|
||||
}
|
||||
console.log('')
|
||||
}
|
||||
}
|
||||
|
||||
dumpRegistry()
|
||||
console.log('')
|
||||
orchestrate().then(
|
||||
() => {
|
||||
console.log('✓ 编排完成:3 页面全部通过校验,setup 数据流正常')
|
||||
},
|
||||
(e) => {
|
||||
console.error('✗ 编排失败:', e)
|
||||
process.exit(1)
|
||||
},
|
||||
)
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"allowImportingTsExtensions": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": ["registry", "pages", "scripts"]
|
||||
}
|
||||
Reference in New Issue
Block a user