mirror of
https://github.com/qingchencloud/clawpanel.git
synced 2026-05-29 04:10:00 +08:00
Cherry-pick the still-relevant fixes from recent draft PRs without pulling in stale release/docs changes: - serialize dashboard data loads to avoid concurrent config self-heal writes - preserve valid per-model default blocks during dashboard model self-heal - pass structured humanizeError results directly to toast for model import scan failures - align frontend kernel isLatest with suffix-aware recommended version ordering Verification: - node --test tests/*.test.js - npm run build
208 lines
8.4 KiB
JavaScript
208 lines
8.4 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, recommendedIsNewer } from '../src/lib/kernel.js'
|
||
import { FEATURE_CATALOG, KERNEL_FLOOR, KERNEL_TARGET } 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 atOfficial = buildSnapshot('openclaw', KERNEL_TARGET.openclaw.official)
|
||
const atChinese = buildSnapshot('openclaw', KERNEL_TARGET.openclaw.chinese)
|
||
const aboveTarget = buildSnapshot('openclaw', '2026.6.0')
|
||
const belowTarget = buildSnapshot('openclaw', '2026.5.18')
|
||
assert.equal(atOfficial.isLatest, true)
|
||
assert.equal(atChinese.isLatest, true)
|
||
assert.equal(aboveTarget.isLatest, true)
|
||
assert.equal(belowTarget.isLatest, false)
|
||
})
|
||
|
||
test('recommendedIsNewer matches suffix patch ordering', () => {
|
||
assert.equal(recommendedIsNewer('2026.5.18-zh.2', '2026.5.18-zh.1'), true)
|
||
assert.equal(recommendedIsNewer('2026.5.18-zh.1', '2026.5.18-zh.2'), false)
|
||
assert.equal(recommendedIsNewer('2026.5.18-zh.1', '2026.5.18-zh.1'), false)
|
||
assert.equal(recommendedIsNewer('2026.5.19', '2026.5.18-zh.9'), true)
|
||
})
|
||
|
||
// ============================================================================
|
||
// 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}`,
|
||
)
|
||
}
|
||
})
|