/**
* 内核版本与特性门控的单元测试
*
* 运行: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 . camelCase', () => {
for (const id of Object.keys(FEATURE_CATALOG)) {
assert.match(id, /^[a-z]+\.[a-zA-Z0-9]+$/, `${id} should match .`)
}
})
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}`,
)
}
})