mirror of
https://github.com/qingchencloud/clawpanel.git
synced 2026-07-03 05:31:33 +08:00
feat: prepare v0.18.0 release
This commit is contained in:
@@ -9,6 +9,7 @@ import {
|
||||
resolveOpenclawCliInput,
|
||||
quarantineOpenclawPathForWeb,
|
||||
readJsonFileRelaxed,
|
||||
shouldFallbackStandaloneToNpm,
|
||||
} from '../scripts/dev-api.js'
|
||||
|
||||
test('Web API JSON 读取会兼容 UTF-8 BOM', () => {
|
||||
@@ -64,6 +65,25 @@ test('Web API CLI 冲突扫描会排除 standalone 安装', () => {
|
||||
assert.deepEqual(records, [])
|
||||
})
|
||||
|
||||
test('Web API CLI 冲突扫描会排除当前活跃入口', () => {
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'clawpanel-cli-active-'))
|
||||
try {
|
||||
const cliPath = path.join(tmp, process.platform === 'win32' ? 'openclaw.cmd' : 'openclaw')
|
||||
fs.writeFileSync(cliPath, process.platform === 'win32' ? '@echo off\r\n' : '#!/bin/sh\n')
|
||||
|
||||
const records = buildOpenclawPathConflictRecords([{
|
||||
path: cliPath,
|
||||
source: 'unknown',
|
||||
version: '9.9.9',
|
||||
active: true,
|
||||
}])
|
||||
|
||||
assert.deepEqual(records, [])
|
||||
} finally {
|
||||
fs.rmSync(tmp, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
test('Web API CLI 隔离只允许 openclaw 文件并保留可恢复备份', () => {
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'clawpanel-cli-quarantine-'))
|
||||
try {
|
||||
@@ -82,6 +102,22 @@ test('Web API CLI 隔离只允许 openclaw 文件并保留可恢复备份', () =
|
||||
}
|
||||
})
|
||||
|
||||
test('Web API CLI 隔离拒绝当前 Gateway 使用中的入口', () => {
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'clawpanel-cli-quarantine-active-'))
|
||||
try {
|
||||
const cliPath = path.join(tmp, process.platform === 'win32' ? 'openclaw.cmd' : 'openclaw')
|
||||
fs.writeFileSync(cliPath, 'echo openclaw\n')
|
||||
|
||||
assert.throws(
|
||||
() => quarantineOpenclawPathForWeb(cliPath, { activeCliPaths: [cliPath] }),
|
||||
/正在被 Gateway 使用/
|
||||
)
|
||||
assert.equal(fs.existsSync(cliPath), true)
|
||||
} finally {
|
||||
fs.rmSync(tmp, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
test('Web API CLI 隔离拒绝非 openclaw 文件', () => {
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'clawpanel-cli-quarantine-deny-'))
|
||||
try {
|
||||
@@ -95,6 +131,17 @@ test('Web API CLI 隔离拒绝非 openclaw 文件', () => {
|
||||
}
|
||||
})
|
||||
|
||||
test('Web API standalone 当前运行模式失败时不自动降级到 npm', () => {
|
||||
assert.equal(shouldFallbackStandaloneToNpm({ currentInstallMode: 'standalone', method: 'auto' }), false)
|
||||
assert.equal(shouldFallbackStandaloneToNpm({ currentInstallMode: 'standalone', method: 'standalone-r2' }), false)
|
||||
})
|
||||
|
||||
test('Web API 非 standalone 当前运行模式允许 auto 兜底 npm', () => {
|
||||
assert.equal(shouldFallbackStandaloneToNpm({ currentInstallMode: 'npm', method: 'auto' }), true)
|
||||
assert.equal(shouldFallbackStandaloneToNpm({ currentInstallMode: 'unknown', method: 'auto' }), true)
|
||||
assert.equal(shouldFallbackStandaloneToNpm({ currentInstallMode: 'npm', method: 'standalone-github' }), false)
|
||||
})
|
||||
|
||||
test('Web API Windows CLI 解析不会绑定无扩展名 openclaw shim', (t) => {
|
||||
if (process.platform !== 'win32') {
|
||||
t.skip('Windows only')
|
||||
|
||||
16
tests/hermes-group-chat-run-events.test.js
Normal file
16
tests/hermes-group-chat-run-events.test.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { matchesHermesRun } from '../src/engines/hermes/lib/hermes-run-events.js'
|
||||
|
||||
test('matchesHermesRun rejects events before run_id is known', () => {
|
||||
assert.equal(matchesHermesRun(null, 'run_abc'), false)
|
||||
assert.equal(matchesHermesRun(undefined, 'run_abc'), false)
|
||||
})
|
||||
|
||||
test('matchesHermesRun rejects events from a different run', () => {
|
||||
assert.equal(matchesHermesRun('run_a', 'run_b'), false)
|
||||
})
|
||||
|
||||
test('matchesHermesRun accepts events for the active run', () => {
|
||||
assert.equal(matchesHermesRun('run_a', 'run_a'), true)
|
||||
})
|
||||
@@ -52,12 +52,15 @@ test('MODEL_PRESETS contains MiniMax models', () => {
|
||||
assert.ok(MODEL_PRESETS.minimax.length >= 2, 'should have at least 2 MiniMax models')
|
||||
})
|
||||
|
||||
test('MiniMax model presets include M2.7 and M2.5 variants', () => {
|
||||
test('MiniMax model presets include M3 and M2.7 variants', () => {
|
||||
const ids = MODEL_PRESETS.minimax.map(m => m.id)
|
||||
assert.ok(ids.includes('MiniMax-M3'), 'should include MiniMax-M3')
|
||||
assert.ok(ids.includes('MiniMax-M2.7'), 'should include MiniMax-M2.7')
|
||||
assert.ok(ids.includes('MiniMax-M2.7-highspeed'), 'should include MiniMax-M2.7-highspeed')
|
||||
assert.ok(ids.includes('MiniMax-M2.5'), 'should include MiniMax-M2.5')
|
||||
assert.ok(ids.includes('MiniMax-M2.5-highspeed'), 'should include MiniMax-M2.5-highspeed')
|
||||
})
|
||||
|
||||
test('MiniMax M3 is listed as the new default (first entry)', () => {
|
||||
assert.equal(MODEL_PRESETS.minimax[0].id, 'MiniMax-M3', 'MiniMax-M3 should be the first model')
|
||||
})
|
||||
|
||||
test('MiniMax model presets have required fields', () => {
|
||||
@@ -76,11 +79,10 @@ test('MiniMax M2.7 models have 1M context window', () => {
|
||||
assert.equal(m27hs.contextWindow, 1000000)
|
||||
})
|
||||
|
||||
test('MiniMax M2.5 models have 204K context window', () => {
|
||||
const m25 = MODEL_PRESETS.minimax.find(m => m.id === 'MiniMax-M2.5')
|
||||
assert.equal(m25.contextWindow, 204000)
|
||||
const m25hs = MODEL_PRESETS.minimax.find(m => m.id === 'MiniMax-M2.5-highspeed')
|
||||
assert.equal(m25hs.contextWindow, 204000)
|
||||
test('MiniMax M3 has 524K context window', () => {
|
||||
const m3 = MODEL_PRESETS.minimax.find(m => m.id === 'MiniMax-M3')
|
||||
assert.ok(m3, 'should include MiniMax-M3')
|
||||
assert.equal(m3.contextWindow, 524288)
|
||||
})
|
||||
|
||||
test('all model preset groups have valid structure', () => {
|
||||
|
||||
@@ -40,3 +40,37 @@ test('官网公告接口按 displayType 分流并过滤非客户端 surface', ()
|
||||
assert.equal(normalized.notifications[0].title, '客户端通知')
|
||||
assert.equal(normalized.announcements[0].title, '系统公告')
|
||||
})
|
||||
|
||||
test('官网公告接口兼容 modal/notification 类型', () => {
|
||||
const normalized = normalizeSiteMessagePayload({
|
||||
announcements: [
|
||||
{
|
||||
id: 4,
|
||||
type: 'modal',
|
||||
displayType: 'modal',
|
||||
targetSurface: 'client',
|
||||
level: 'warning',
|
||||
title: '客户端弹窗测试',
|
||||
body: '客户端弹窗测试',
|
||||
dismissKey: 'client-modal-202606',
|
||||
updatedAt: '2026-06-06T07:25:40Z',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
type: 'notification',
|
||||
displayType: 'notification',
|
||||
targetSurface: 'client',
|
||||
level: 'info',
|
||||
title: '客户端通知测试',
|
||||
body: '客户端通知测试',
|
||||
dismissKey: 'client-notification-202606',
|
||||
updatedAt: '2026-06-06T07:25:28Z',
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
assert.equal(normalized.notifications.length, 1)
|
||||
assert.equal(normalized.announcements.length, 1)
|
||||
assert.equal(normalized.notifications[0].title, '客户端通知测试')
|
||||
assert.equal(normalized.announcements[0].title, '客户端弹窗测试')
|
||||
})
|
||||
|
||||
24
tests/site-update-source-policy.test.js
Normal file
24
tests/site-update-source-policy.test.js
Normal file
@@ -0,0 +1,24 @@
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFileSync } from 'node:fs'
|
||||
|
||||
const devApi = readFileSync(new URL('../scripts/dev-api.js', import.meta.url), 'utf8')
|
||||
const rustConfig = readFileSync(new URL('../src-tauri/src/commands/config.rs', import.meta.url), 'utf8')
|
||||
|
||||
test('ClawPanel 版本发现只使用官网 API,不再回退 GitHub/Gitee release API', () => {
|
||||
for (const source of [devApi, rustConfig]) {
|
||||
assert.doesNotMatch(
|
||||
source,
|
||||
/api\.github\.com\/repos\/qingchencloud\/clawpanel\/releases\/latest/,
|
||||
'版本发现不应再请求 GitHub releases latest API',
|
||||
)
|
||||
assert.doesNotMatch(
|
||||
source,
|
||||
/gitee\.com\/api\/v5\/repos\/QtCodeCreators\/clawpanel\/releases\/latest/,
|
||||
'版本发现不应再请求 Gitee releases latest API',
|
||||
)
|
||||
}
|
||||
|
||||
assert.match(devApi, /return await getSitePanelUpdate\(\)/)
|
||||
assert.match(rustConfig, /site_latest_for_panel_update\(\)/)
|
||||
})
|
||||
65
tests/web-headless-reload-policy.test.js
Normal file
65
tests/web-headless-reload-policy.test.js
Normal file
@@ -0,0 +1,65 @@
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFileSync } from 'node:fs'
|
||||
|
||||
const tauriApi = readFileSync(new URL('../src/lib/tauri-api.js', import.meta.url), 'utf8')
|
||||
const wsClient = readFileSync(new URL('../src/lib/ws-client.js', import.meta.url), 'utf8')
|
||||
const main = readFileSync(new URL('../src/main.js', import.meta.url), 'utf8')
|
||||
const chat = readFileSync(new URL('../src/pages/chat.js', import.meta.url), 'utf8')
|
||||
|
||||
test('Web/headless 模式配置写入不能隐式 reload Gateway', () => {
|
||||
assert.match(
|
||||
tauriApi,
|
||||
/function\s+_debouncedReloadGateway\(\)\s*\{[\s\S]*?if\s*\(\s*!isTauriRuntime\(\)\s*\)\s*return/,
|
||||
'tauri-api 的防抖 reload 必须在 Web/headless 模式直接跳过',
|
||||
)
|
||||
})
|
||||
|
||||
test('Web/headless 模式自动配对重连不能隐式 reload Gateway', () => {
|
||||
assert.match(
|
||||
wsClient,
|
||||
/import\s+\{\s*api\s*,\s*isTauriRuntime\s*\}\s+from\s+['"]\.\/tauri-api\.js['"]/,
|
||||
'ws-client 必须能判断当前是否为 Tauri 桌面端',
|
||||
)
|
||||
assert.match(
|
||||
wsClient,
|
||||
/if\s*\(\s*isTauriRuntime\(\)\s*\)\s*\{[\s\S]*?await\s+api\.reloadGateway\(\)/,
|
||||
'自动配对后的 reload 只能在 Tauri 桌面端执行',
|
||||
)
|
||||
})
|
||||
|
||||
test('Web/headless 模式启动自动连接不能隐式 reload Gateway', () => {
|
||||
assert.match(
|
||||
main,
|
||||
/if\s*\(\s*needReload\s*&&\s*isTauriRuntime\(\)\s*\)\s*\{[\s\S]*?await\s+api\.reloadGateway\(\)/,
|
||||
'启动自动连接合并 reload 只能在 Tauri 桌面端执行',
|
||||
)
|
||||
})
|
||||
|
||||
test('Web/headless 模式聊天连接修复不自动 reload Gateway', () => {
|
||||
assert.match(
|
||||
chat,
|
||||
/if\s*\(\s*isTauriRuntime\(\)\s*\)\s*\{[\s\S]*?await\s+api\.reloadGateway\(\)/,
|
||||
'聊天页连接修复按钮只能在 Tauri 桌面端自动 reload',
|
||||
)
|
||||
})
|
||||
|
||||
test('聊天发送按钮会在输入状态变化时重新同步 disabled 状态', () => {
|
||||
assert.match(
|
||||
chat,
|
||||
/<button class="chat-send-btn" id="chat-send-btn" type="button" disabled>/,
|
||||
'发送按钮必须是普通按钮,避免表单/浏览器默认行为干扰点击状态',
|
||||
)
|
||||
for (const eventName of ['compositionend', 'change', 'keyup']) {
|
||||
assert.match(
|
||||
chat,
|
||||
new RegExp(`_textarea\\.addEventListener\\('${eventName}',\\s*updateSendState\\)`),
|
||||
`textarea ${eventName} 事件必须同步发送按钮状态`,
|
||||
)
|
||||
}
|
||||
assert.match(
|
||||
chat,
|
||||
/requestAnimationFrame\(updateSendState\)/,
|
||||
'页面初始化后需要再同步一次发送按钮状态,覆盖自动填充或恢复输入内容',
|
||||
)
|
||||
})
|
||||
Reference in New Issue
Block a user