Files
clawpanel/tests/kernel.test.js
晴天 a50000a933 fix(gateway): support latest hello server version payload
OpenClaw 2026.5.18/2026.5.19 still uses Gateway WS protocol v4
(PROTOCOL_VERSION=4, MIN_CLIENT_PROTOCOL_VERSION=4), so there is no v5
handshake to implement. The compatibility break is the hello payload
shape: current upstream sends the runtime version at `hello.server.version`
while ClawPanel only read the old flat `hello.serverVersion` field.

Read both shapes so latest kernels keep populating wsClient.serverVersion,
which in turn keeps Dashboard display, kernel snapshot feature gates and
isLatest checks working after the WebSocket handshake succeeds.

Also bump the recommended OpenClaw targets to the current npm latests:
- official: 2026.5.19
- chinese: 2026.5.18-zh.1

Verification:
- node --test tests/kernel.test.js
- npm run build
- manual module check: simulated both hello.server.version and legacy
  hello.serverVersion payloads, both report serverVersion and protocol v4
2026-05-21 14:25:10 +08:00

201 lines
8.0 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 内核版本与特性门控的单元测试
*
* 运行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.19')
const at_chinese_target = buildSnapshot('openclaw', '2026.5.18-zh.1')
const above_target = buildSnapshot('openclaw', '2026.6.0')
const below_target = buildSnapshot('openclaw', '2026.5.18')
assert.equal(at_target.isLatest, true)
assert.equal(at_chinese_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}`,
)
}
})