mirror of
https://github.com/qingchencloud/clawpanel.git
synced 2026-08-07 06:33:58 +08:00
chore(release): prepare v0.19.2
This commit is contained in:
20
tests/dev-api-gateway-probe.test.js
Normal file
20
tests/dev-api-gateway-probe.test.js
Normal file
@@ -0,0 +1,20 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import net from 'node:net'
|
||||
import test from 'node:test'
|
||||
|
||||
import { probeTcpPort } from '../scripts/dev-api.js'
|
||||
|
||||
test('Web Gateway 端口探测在 ESM 运行时返回布尔值', async () => {
|
||||
const server = net.createServer(socket => socket.end())
|
||||
await new Promise((resolve, reject) => {
|
||||
server.once('error', reject)
|
||||
server.listen(0, '127.0.0.1', resolve)
|
||||
})
|
||||
|
||||
const address = server.address()
|
||||
assert.equal(typeof address, 'object')
|
||||
assert.equal(await probeTcpPort(address.port, '127.0.0.1', 1000), true)
|
||||
|
||||
await new Promise((resolve, reject) => server.close(error => error ? reject(error) : resolve()))
|
||||
assert.equal(await probeTcpPort(address.port, '127.0.0.1', 1000), false)
|
||||
})
|
||||
117
tests/dev-api-models-sync.test.js
Normal file
117
tests/dev-api-models-sync.test.js
Normal file
@@ -0,0 +1,117 @@
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import fs from 'node:fs'
|
||||
import os from 'node:os'
|
||||
import path from 'node:path'
|
||||
|
||||
import { syncProvidersToAgentModels } from '../scripts/dev-api.js'
|
||||
|
||||
function writeAgentModels(root, agentId, value) {
|
||||
const file = path.join(root, 'agents', agentId, 'agent', 'models.json')
|
||||
fs.mkdirSync(path.dirname(file), { recursive: true })
|
||||
fs.writeFileSync(file, JSON.stringify(value, null, 2))
|
||||
return file
|
||||
}
|
||||
|
||||
function readJson(file) {
|
||||
return JSON.parse(fs.readFileSync(file, 'utf8'))
|
||||
}
|
||||
|
||||
test('Web API 同步 providers 时会更新每个 Agent 的连接信息和上下文元数据', () => {
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'clawpanel-models-sync-'))
|
||||
try {
|
||||
const oldModels = {
|
||||
providers: {
|
||||
lmstudio: {
|
||||
baseUrl: 'http://127.0.0.1:1234/v1-old',
|
||||
apiKey: 'old-key',
|
||||
api: 'openai-completions',
|
||||
models: [{
|
||||
id: 'qwen-local',
|
||||
name: 'Qwen Local',
|
||||
contextWindow: 50000,
|
||||
contextTokens: 50000,
|
||||
maxTokens: 4096,
|
||||
customRuntimeFlag: true,
|
||||
}],
|
||||
modelOverrides: {
|
||||
'qwen-local': { contextWindow: 50000, contextTokens: 50000 },
|
||||
},
|
||||
},
|
||||
stale: { baseUrl: 'http://stale.example/v1', models: [{ id: 'old' }] },
|
||||
},
|
||||
}
|
||||
const mainPath = writeAgentModels(tmp, 'main', oldModels)
|
||||
const workerPath = writeAgentModels(tmp, 'worker', oldModels)
|
||||
|
||||
const result = syncProvidersToAgentModels({
|
||||
agents: { list: [{ id: 'worker' }] },
|
||||
models: {
|
||||
providers: {
|
||||
lmstudio: {
|
||||
baseUrl: 'http://127.0.0.1:1234/v1',
|
||||
apiKey: 'new-key',
|
||||
api: 'openai-completions',
|
||||
models: [{
|
||||
id: 'qwen-local',
|
||||
name: 'Qwen Local',
|
||||
contextWindow: 131072,
|
||||
contextTokens: 131072,
|
||||
maxTokens: 8192,
|
||||
}, { id: 'new-local', name: 'New Local', contextWindow: 65536 }],
|
||||
},
|
||||
},
|
||||
},
|
||||
}, tmp)
|
||||
|
||||
assert.deepEqual(result.updated.sort(), [mainPath, workerPath].sort())
|
||||
for (const file of [mainPath, workerPath]) {
|
||||
const synced = readJson(file)
|
||||
assert.equal(synced.providers.lmstudio.baseUrl, 'http://127.0.0.1:1234/v1')
|
||||
assert.equal(synced.providers.lmstudio.apiKey, 'new-key')
|
||||
assert.equal(synced.providers.stale, undefined)
|
||||
const local = synced.providers.lmstudio.models.find(model => model.id === 'qwen-local')
|
||||
assert.equal(local.contextWindow, 131072)
|
||||
assert.equal(local.contextTokens, 131072)
|
||||
assert.equal(local.maxTokens, 8192)
|
||||
assert.equal(local.customRuntimeFlag, true)
|
||||
assert.equal(synced.providers.lmstudio.modelOverrides['qwen-local'].contextWindow, 131072)
|
||||
assert.equal(synced.providers.lmstudio.modelOverrides['qwen-local'].contextTokens, 131072)
|
||||
assert.equal(synced.providers.lmstudio.models.some(model => model.id === 'new-local'), true)
|
||||
}
|
||||
} finally {
|
||||
fs.rmSync(tmp, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
test('Web API 同步保留 Agent 运行时手动添加的模型', () => {
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'clawpanel-models-sync-'))
|
||||
try {
|
||||
const modelsPath = writeAgentModels(tmp, 'main', {
|
||||
providers: {
|
||||
lmstudio: {
|
||||
models: [
|
||||
{ id: 'qwen-local', contextWindow: 50000 },
|
||||
{ id: 'manual-only', contextWindow: 32768 },
|
||||
],
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
syncProvidersToAgentModels({
|
||||
models: {
|
||||
providers: {
|
||||
lmstudio: {
|
||||
models: [{ id: 'qwen-local', contextWindow: 65536 }],
|
||||
},
|
||||
},
|
||||
},
|
||||
}, tmp)
|
||||
|
||||
const synced = readJson(modelsPath)
|
||||
assert.equal(synced.providers.lmstudio.models.find(model => model.id === 'qwen-local').contextWindow, 65536)
|
||||
assert.equal(synced.providers.lmstudio.models.some(model => model.id === 'manual-only'), true)
|
||||
} finally {
|
||||
fs.rmSync(tmp, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
29
tests/docker-base-user.test.js
Normal file
29
tests/docker-base-user.test.js
Normal file
@@ -0,0 +1,29 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import fs from 'node:fs'
|
||||
import test from 'node:test'
|
||||
|
||||
const dockerfile = fs.readFileSync(new URL('../Dockerfile', import.meta.url), 'utf8')
|
||||
const dockerignore = fs.readFileSync(new URL('../.dockerignore', import.meta.url), 'utf8')
|
||||
const compose = fs.readFileSync(new URL('../docker-compose.yml', import.meta.url), 'utf8')
|
||||
|
||||
test('Docker 生产镜像复用官方 node 用户,避免 Alpine UID/GID 1000 冲突', () => {
|
||||
assert.doesNotMatch(dockerfile, /addgroup\s+-g\s+1000/)
|
||||
assert.match(dockerfile, /--chown=node:node/)
|
||||
assert.match(dockerfile, /chown -R node:node \/app/)
|
||||
})
|
||||
|
||||
test('Docker 构建上下文和生产镜像包含 Web API 运行时依赖', () => {
|
||||
assert.match(dockerignore, /!public\/\*\*/)
|
||||
assert.match(dockerfile, /COPY public\/ \.\/public\//)
|
||||
assert.match(dockerignore, /!scripts\/dev-api\.js/)
|
||||
assert.match(dockerignore, /!scripts\/media-background-queue\.js/)
|
||||
assert.match(dockerignore, /!scripts\/lib\/\*\*/)
|
||||
assert.match(dockerfile, /\/build\/src\/lib\/model-presets\.js/)
|
||||
})
|
||||
|
||||
test('Docker 健康检查跟随 Web 自定义端口', () => {
|
||||
assert.match(dockerfile, /ENV PORT=1420/)
|
||||
assert.match(dockerfile, /localhost:\$\{PORT:-1420\}\/__api\/health/)
|
||||
assert.match(compose, /PORT=\$\{CLAWPANEL_PORT:-1420\}/)
|
||||
assert.match(compose, /localhost:\$\$\{PORT:-1420\}\/__api\/health/)
|
||||
})
|
||||
46
tests/hermes-profiles.test.js
Normal file
46
tests/hermes-profiles.test.js
Normal file
@@ -0,0 +1,46 @@
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFileSync } from 'node:fs'
|
||||
|
||||
import { parseHermesProfileListOutput } from '../scripts/dev-api.js'
|
||||
|
||||
const profilePage = readFileSync(new URL('../src/engines/hermes/pages/profiles.js', import.meta.url), 'utf8')
|
||||
const chatPage = readFileSync(new URL('../src/engines/hermes/pages/chat.js', import.meta.url), 'utf8')
|
||||
const componentCss = readFileSync(new URL('../src/style/components.css', import.meta.url), 'utf8')
|
||||
|
||||
test('Hermes 新版 profile list 的星号格式可以正确解析', () => {
|
||||
const result = parseHermesProfileListOutput(`Available profiles:\n default\n* work\n dev\n personal`)
|
||||
|
||||
assert.equal(result.active, 'work')
|
||||
assert.deepEqual(result.profiles.map(profile => profile.name), ['default', 'work', 'dev', 'personal'])
|
||||
assert.equal(result.profiles.find(profile => profile.name === 'work')?.active, true)
|
||||
assert.equal(result.profiles.find(profile => profile.name === 'default')?.active, false)
|
||||
})
|
||||
|
||||
test('Hermes 旧版 profile 表格仍保留模型和 Gateway 状态', () => {
|
||||
const result = parseHermesProfileListOutput([
|
||||
'Profile Model Gateway Alias',
|
||||
'◆ default anthropic/claude stopped —',
|
||||
' work openai/gpt running work',
|
||||
].join('\n'))
|
||||
|
||||
assert.equal(result.active, 'default')
|
||||
assert.deepEqual(result.profiles, [
|
||||
{ name: 'default', active: true, model: 'anthropic/claude', gatewayRunning: false, alias: '' },
|
||||
{ name: 'work', active: false, model: 'openai/gpt', gatewayRunning: true, alias: 'work' },
|
||||
])
|
||||
})
|
||||
|
||||
test('Hermes Profile 页面提供按 Profile 写入模型的入口', () => {
|
||||
assert.match(profilePage, /hermesDashboardApi\('PUT', `\/api\/profiles\/\$\{encodeURIComponent\(name\)\}\/model`/)
|
||||
assert.match(profilePage, /data-action="configure"/)
|
||||
})
|
||||
|
||||
test('Hermes 聊天页提供直接启动 Gateway 的入口', () => {
|
||||
assert.match(chatPage, /id="hm-chat-start-gateway"/)
|
||||
assert.match(chatPage, /hermesGatewayAction\('start'\)/)
|
||||
})
|
||||
|
||||
test('Hermes Profile 移动端操作按钮满足触控高度', () => {
|
||||
assert.match(componentCss, /\.lazy-deps-card-actions \.btn\s*\{[^}]*min-height:\s*40px/s)
|
||||
})
|
||||
@@ -28,6 +28,10 @@ test('Linux Web 构建后必须重启已有服务并输出实际版本', () => {
|
||||
assert.match(script, /ClawPanel 版本:.*PANEL_VERSION/)
|
||||
})
|
||||
|
||||
test('Linux Web 部署允许通过环境变量切换到未占用端口', () => {
|
||||
assert.match(script, /PANEL_PORT="\$\{CLAWPANEL_PORT:-\$\{PANEL_PORT:-1420\}\}"/)
|
||||
})
|
||||
|
||||
test('Linux Web 升级文档必须区分 system 与 user 服务并提供可诊断命令', () => {
|
||||
for (const [name, content] of [
|
||||
['README.md', readme],
|
||||
|
||||
@@ -17,8 +17,18 @@ const originalApi = {
|
||||
readOpenclawConfig: api.readOpenclawConfig,
|
||||
writeOpenclawConfig: api.writeOpenclawConfig,
|
||||
revealModelChannelKey: api.revealModelChannelKey,
|
||||
probeGatewayPort: api.probeGatewayPort,
|
||||
restartGateway: api.restartGateway,
|
||||
reloadGateway: api.reloadGateway,
|
||||
}
|
||||
|
||||
test.beforeEach(() => {
|
||||
// 运行时应用动作由专门测试覆盖;其余同步测试保持纯配置回读。
|
||||
api.probeGatewayPort = async () => false
|
||||
api.restartGateway = async () => true
|
||||
api.reloadGateway = async () => true
|
||||
})
|
||||
|
||||
function restoreApi() {
|
||||
Object.assign(api, originalApi)
|
||||
storage.clear()
|
||||
@@ -138,6 +148,23 @@ test('同步旧 Codex 渠道时写入 7.1 正式 API 类型', async () => {
|
||||
assert.equal(written.models.providers.legacy.api, 'openai-chatgpt-responses')
|
||||
})
|
||||
|
||||
test('Web 模型渠道同步后会重启运行中的 Gateway 以加载 Agent 模型注册表', async () => {
|
||||
let written = null
|
||||
let restartCount = 0
|
||||
api.probeGatewayPort = async () => true
|
||||
api.restartGateway = async () => { restartCount += 1 }
|
||||
api.revealModelChannelKey = async () => 'sk-test'
|
||||
api.readOpenclawConfig = async () => written || ({ models: { providers: {} } })
|
||||
api.writeOpenclawConfig = async config => { written = config }
|
||||
|
||||
await channels.syncChannelToOpenclaw({
|
||||
id: 'lmstudio', name: 'LM Studio', baseUrl: 'http://127.0.0.1:1234/v1',
|
||||
apiType: 'openai-completions', models: [{ id: 'qwen-local' }], defaultModel: 'qwen-local',
|
||||
})
|
||||
|
||||
assert.equal(restartCount, 1)
|
||||
})
|
||||
|
||||
test('OpenClaw 同步必须通过目标配置回读核对', async () => {
|
||||
api.revealModelChannelKey = async () => 'sk-test'
|
||||
api.readOpenclawConfig = async () => ({ models: { providers: {} } })
|
||||
|
||||
Reference in New Issue
Block a user