mirror of
https://github.com/qingchencloud/clawpanel.git
synced 2026-05-27 19:30:15 +08:00
发布 0.15.0: - 新增内核版本兼容层、特性门控、低版本阻断和升级提示 - 新增 PATH 中 OpenClaw CLI 冲突检测、隔离与恢复 - 修复 Hermes Gateway loopback 自动拉起与 /v1/runs 诊断 - 修复 standalone 一键安装包在 About/仪表盘显示未知版本 - 同步 OpenClaw 2026.5.6 推荐版本和热更新 minAppVersion - 补齐本地 JS/Rust 测试与发布前检查说明 验证: - npm run build - node --test tests/*.test.js - node --check src/scripts JS 文件 - cargo fmt --all -- --check - cargo check - cargo clippy --all-targets -- -D warnings - cargo test
199 lines
7.9 KiB
JavaScript
199 lines
7.9 KiB
JavaScript
/**
|
||
* 内核版本与特性门控的单元测试
|
||
*
|
||
* 运行:node --test tests/kernel.test.js
|
||
*
|
||
* 注意:本测试只覆盖 kernel.js 中的 **纯函数**(parseVersion / versionGte / buildSnapshot)。
|
||
* 涉及 wsClient 订阅 / DOM 的状态函数无法在 node 环境直接测试,留给 e2e。
|
||
*/
|
||
import test from 'node:test'
|
||
import assert from 'node:assert/strict'
|
||
|
||
import { parseVersion, versionGte, buildSnapshot } from '../src/lib/kernel.js'
|
||
import { FEATURE_CATALOG, KERNEL_FLOOR } from '../src/lib/feature-catalog.js'
|
||
|
||
// ============================================================================
|
||
// parseVersion
|
||
// ============================================================================
|
||
|
||
test('parseVersion handles standard semver', () => {
|
||
assert.deepEqual(parseVersion('1.2.3'), [1, 2, 3])
|
||
assert.deepEqual(parseVersion('0.0.1'), [0, 0, 1])
|
||
assert.deepEqual(parseVersion('2026.5.6'), [2026, 5, 6])
|
||
})
|
||
|
||
test('parseVersion strips -zh / -beta suffix', () => {
|
||
assert.deepEqual(parseVersion('2026.5.6-zh.2'), [2026, 5, 6])
|
||
assert.deepEqual(parseVersion('2026.5.6-beta.1'), [2026, 5, 6])
|
||
assert.deepEqual(parseVersion('1.0.0-rc.1'), [1, 0, 0])
|
||
})
|
||
|
||
test('parseVersion pads short version', () => {
|
||
assert.deepEqual(parseVersion('1'), [1, 0, 0])
|
||
assert.deepEqual(parseVersion('1.2'), [1, 2, 0])
|
||
})
|
||
|
||
test('parseVersion returns null on invalid input', () => {
|
||
assert.equal(parseVersion(null), null)
|
||
assert.equal(parseVersion(''), null)
|
||
assert.equal(parseVersion(undefined), null)
|
||
assert.equal(parseVersion('not-a-version'), null)
|
||
assert.equal(parseVersion('a.b.c'), null)
|
||
})
|
||
|
||
test('parseVersion only takes first 3 segments', () => {
|
||
assert.deepEqual(parseVersion('1.2.3.4.5'), [1, 2, 3])
|
||
})
|
||
|
||
// ============================================================================
|
||
// versionGte
|
||
// ============================================================================
|
||
|
||
test('versionGte returns true for equal versions', () => {
|
||
assert.equal(versionGte('1.0.0', '1.0.0'), true)
|
||
assert.equal(versionGte('2026.5.6', '2026.5.6'), true)
|
||
})
|
||
|
||
test('versionGte returns true for higher major', () => {
|
||
assert.equal(versionGte('2.0.0', '1.99.99'), true)
|
||
assert.equal(versionGte('2026.0.0', '2025.99.99'), true)
|
||
})
|
||
|
||
test('versionGte returns true for higher minor when major equal', () => {
|
||
assert.equal(versionGte('1.5.0', '1.4.99'), true)
|
||
assert.equal(versionGte('2026.5.0', '2026.4.21'), true)
|
||
})
|
||
|
||
test('versionGte returns true for higher patch when major+minor equal', () => {
|
||
assert.equal(versionGte('1.0.5', '1.0.4'), true)
|
||
assert.equal(versionGte('2026.5.6', '2026.5.5'), true)
|
||
})
|
||
|
||
test('versionGte returns false for lower versions', () => {
|
||
assert.equal(versionGte('1.0.0', '2.0.0'), false)
|
||
assert.equal(versionGte('2026.4.9', '2026.5.6'), false)
|
||
assert.equal(versionGte('2026.3.2', '2026.5.6'), false)
|
||
})
|
||
|
||
test('versionGte ignores -zh suffix correctly', () => {
|
||
assert.equal(versionGte('2026.5.6-zh.2', '2026.5.6'), true)
|
||
assert.equal(versionGte('2026.5.6', '2026.5.6-zh.2'), true)
|
||
assert.equal(versionGte('2026.4.9-zh.2', '2026.5.6'), false)
|
||
})
|
||
|
||
test('versionGte returns false when input is unparseable', () => {
|
||
assert.equal(versionGte(null, '1.0.0'), false)
|
||
assert.equal(versionGte('1.0.0', null), false)
|
||
assert.equal(versionGte('foo', 'bar'), false)
|
||
})
|
||
|
||
// ============================================================================
|
||
// buildSnapshot
|
||
// ============================================================================
|
||
|
||
test('buildSnapshot constructs correct shape for known engine + version', () => {
|
||
const snap = buildSnapshot('openclaw', '2026.5.6')
|
||
assert.equal(snap.engine, 'openclaw')
|
||
assert.equal(snap.version, '2026.5.6')
|
||
assert.equal(snap.versionBase, '2026.5.6')
|
||
assert.equal(snap.variant, 'official')
|
||
assert.equal(snap.aboveFloor, true)
|
||
assert.equal(snap.floor, KERNEL_FLOOR.openclaw)
|
||
assert.ok(snap.features instanceof Set)
|
||
})
|
||
|
||
test('buildSnapshot detects chinese variant', () => {
|
||
const snap = buildSnapshot('openclaw', '2026.5.6-zh.2')
|
||
assert.equal(snap.variant, 'chinese')
|
||
assert.equal(snap.versionBase, '2026.5.6')
|
||
assert.equal(snap.versionLabel, '2026.5.6 汉化')
|
||
})
|
||
|
||
test('buildSnapshot.features contains 5.6 features for 5.6 kernel', () => {
|
||
const snap = buildSnapshot('openclaw', '2026.5.6')
|
||
assert.ok(snap.features.has('sessions.truncation'), 'sessions.truncation should be enabled on 5.6')
|
||
assert.ok(snap.features.has('agents.runtime'), 'agents.runtime should be enabled on 5.6')
|
||
assert.ok(snap.features.has('memory.statusDeepSplit'), 'memory.statusDeepSplit should be enabled on 5.6')
|
||
assert.ok(snap.features.has('doctor.deepSupervisor'), 'doctor.deepSupervisor should be enabled on 5.6')
|
||
})
|
||
|
||
test('buildSnapshot.features excludes 5.6 features on 4.9 kernel', () => {
|
||
const snap = buildSnapshot('openclaw', '2026.4.9')
|
||
assert.equal(snap.features.has('sessions.truncation'), false, 'sessions.truncation should NOT be enabled on 4.9')
|
||
assert.equal(snap.features.has('agents.runtime'), false, 'agents.runtime should NOT be enabled on 4.9')
|
||
assert.equal(snap.features.has('memory.statusDeepSplit'), false)
|
||
assert.equal(snap.features.has('doctor.deepSupervisor'), false)
|
||
})
|
||
|
||
test('buildSnapshot.aboveFloor is false for kernel below floor', () => {
|
||
const snap = buildSnapshot('openclaw', '2026.2.0')
|
||
assert.equal(snap.aboveFloor, false, '2026.2.0 should be below floor 2026.3.2')
|
||
})
|
||
|
||
test('buildSnapshot.aboveFloor is true at exactly floor', () => {
|
||
const snap = buildSnapshot('openclaw', KERNEL_FLOOR.openclaw)
|
||
assert.equal(snap.aboveFloor, true)
|
||
})
|
||
|
||
test('buildSnapshot returns null version when input is null', () => {
|
||
const snap = buildSnapshot('openclaw', null)
|
||
assert.equal(snap.version, null)
|
||
assert.equal(snap.aboveFloor, false)
|
||
assert.equal(snap.features.size, 0, 'no features enabled when version unknown')
|
||
})
|
||
|
||
test('buildSnapshot.features only includes current engine', () => {
|
||
const snap = buildSnapshot('hermes', '0.13.0')
|
||
// openclaw 特性应该全部被排除
|
||
for (const id of snap.features) {
|
||
const def = FEATURE_CATALOG[id]
|
||
assert.equal(def.engine, 'hermes', `${id} should belong to hermes engine`)
|
||
}
|
||
})
|
||
|
||
test('buildSnapshot edge case: version slightly below 5.6 feature requirement', () => {
|
||
// sessions.truncation requires 2026.5.4
|
||
const at = buildSnapshot('openclaw', '2026.5.4')
|
||
const below = buildSnapshot('openclaw', '2026.5.3')
|
||
assert.equal(at.features.has('sessions.truncation'), true)
|
||
assert.equal(below.features.has('sessions.truncation'), false)
|
||
})
|
||
|
||
test('buildSnapshot.isLatest works against KERNEL_TARGET', () => {
|
||
const at_target = buildSnapshot('openclaw', '2026.5.6')
|
||
const above_target = buildSnapshot('openclaw', '2026.6.0')
|
||
const below_target = buildSnapshot('openclaw', '2026.5.5')
|
||
assert.equal(at_target.isLatest, true)
|
||
assert.equal(above_target.isLatest, true)
|
||
assert.equal(below_target.isLatest, false)
|
||
})
|
||
|
||
// ============================================================================
|
||
// FEATURE_CATALOG sanity
|
||
// ============================================================================
|
||
|
||
test('FEATURE_CATALOG: every entry has engine and minVersion', () => {
|
||
for (const [id, def] of Object.entries(FEATURE_CATALOG)) {
|
||
assert.ok(def.engine, `${id} missing engine`)
|
||
assert.ok(def.minVersion, `${id} missing minVersion`)
|
||
assert.ok(parseVersion(def.minVersion), `${id} has unparseable minVersion: ${def.minVersion}`)
|
||
}
|
||
})
|
||
|
||
test('FEATURE_CATALOG: id format is <area>.<feature> camelCase', () => {
|
||
for (const id of Object.keys(FEATURE_CATALOG)) {
|
||
assert.match(id, /^[a-z]+\.[a-zA-Z0-9]+$/, `${id} should match <area>.<featureCamelCase>`)
|
||
}
|
||
})
|
||
|
||
test('FEATURE_CATALOG: all openclaw features minVersion >= floor', () => {
|
||
for (const [id, def] of Object.entries(FEATURE_CATALOG)) {
|
||
if (def.engine !== 'openclaw') continue
|
||
assert.equal(
|
||
versionGte(def.minVersion, KERNEL_FLOOR.openclaw),
|
||
true,
|
||
`${id} minVersion ${def.minVersion} is below floor ${KERNEL_FLOOR.openclaw}`,
|
||
)
|
||
}
|
||
})
|