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:
hellow-da-sb
2026-09-03 00:48:10 +08:00
commit 8c93bde5f6
30 changed files with 1289 additions and 0 deletions
+18
View File
@@ -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)
},
},
})
+48
View File
@@ -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)
},
)
+15
View File
@@ -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"]
}