Files
u-claw/tests/windows-launchers.test.mjs
hfshfg 3439db32ad feat(portable): 内网/局域网可用性工具包 + 启动绕代理 (v2.1.10)
便携版在内网/离线/受控浏览器环境下"配不上、用不了、查不清"的三个痛点工具化,
Win + Mac 双端,纯 Node 零依赖脚本,中文提示由 node 打印(.bat 保持纯 ASCII,
.command 保持 LF),三端真机验证(本机 Win / Mac mini / 客户机 pc-7512)。

新增工具:
- lib/intranet-check.mjs (+ Windows-IntranetFix.bat / Mac-IntranetFix.command)
  一键体检: 代理env + 直连可达 + 真发一条对话,分清"网络不通 vs 配置错"。
- lib/setup-local-model.mjs (+ Windows-LocalModel.bat / Mac-LocalModel.command)
  纯命令行配 Ollama / newapi 并当场实测,绕开打不开的 Control UI;写前自动备份+merge。
- lib/resolve-no-proxy.mjs 接入 Windows-Start.bat / Mac-Start.command:
  把配置里的模型 host 写进 NO_PROXY,避免系统代理劫持内网模型请求
  (OpenClaw 自身只 bypass loopback CDP,不管模型 host)。
- OpenClaw-Doctor.bat / Mac-OpenClaw-Doctor.command: 官方 doctor 的隔离进阶入口
  (只读、非交互;doctor 实跑慢且 TTY-only,故不进客户一键流程)。

测试: tests/windows-launchers.test.mjs 新增回归
- 客户面 .bat 必须纯 ASCII (UTF-8 中文会被 GBK cmd 读乱,报 usebackq 不是命令)
- macOS .command 必须 LF-only (CRLF 触发 bad interpreter: /bin/bash^M)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 18:53:21 +08:00

174 lines
6.0 KiB
JavaScript

import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { join } from 'node:path';
import test from 'node:test';
import assert from 'node:assert/strict';
const repoRoot = fileURLToPath(new URL('..', import.meta.url));
function readRepoFile(...parts) {
return readFileSync(join(repoRoot, ...parts), 'utf8');
}
function lineOf(content, needle) {
const lines = content.split(/\r?\n/);
const index = lines.findIndex((line) => line.includes(needle));
assert.notEqual(index, -1, `missing line containing: ${needle}`);
return lines[index];
}
test('Windows-Start dependency fallback text escapes parentheses inside IF block', () => {
const script = readRepoFile('portable', 'Windows-Start.bat');
assert.match(
lineOf(script, 'Falling back to npm install'),
/\^\(USB drives may take 20\+ minutes\^\)\./,
);
assert.match(
lineOf(script, 'pre-installed deps'),
/\^\(~200 MB\^\)\./,
);
});
test('portable Windows launchers disable OpenClaw bonjour discovery', () => {
for (const scriptName of [
'Windows-Start.bat',
'Windows-Menu.bat',
'Windows-Install.bat',
]) {
const script = readRepoFile('portable', scriptName);
assert.match(
script,
/OPENCLAW_DISABLE_BONJOUR=1/,
`${scriptName} should disable bonjour discovery`,
);
}
});
test('Windows startup keeps Config Center available even after model setup', () => {
const script = readRepoFile('portable', 'Windows-Start.bat');
assert.match(
script,
/Opening Config Center[\s\S]*start "" http:\/\/127\.0\.0\.1:18788\//,
'Windows-Start.bat should always open Config Center for model/channel changes',
);
assert.doesNotMatch(
script,
/if not defined MODEL_CONFIGURED/,
'Config Center should not be gated on first-time setup only',
);
});
test('Windows gateway fallback does not force-open Dashboard', () => {
const script = readRepoFile('portable', 'lib', 'wait-gateway.bat');
assert.match(
script,
/:timeout[\s\S]*start "" http:\/\/127\.0\.0\.1:18788\//,
'wait-gateway.bat should return users to Config Center on timeout',
);
assert.doesNotMatch(
script,
/#token=uclaw/,
'fallback should not push configured users straight into Dashboard',
);
assert.doesNotMatch(
lineOf(script, ':ready') + '\n' + lineOf(script, 'exit /b 0'),
/start ""/,
'ready fallback should not open duplicate browser tabs',
);
});
test('portable launchers route configured model hosts around the system proxy', () => {
const winStart = readRepoFile('portable', 'Windows-Start.bat');
assert.match(
winStart,
/resolve-no-proxy\.mjs[\s\S]*UCLAW_NO_PROXY[\s\S]*set "NO_PROXY=/,
'Windows-Start.bat should set NO_PROXY from resolve-no-proxy.mjs',
);
const macStart = readRepoFile('portable', 'Mac-Start.command');
assert.match(
macStart,
/resolve-no-proxy\.mjs[\s\S]*export NO_PROXY=/,
'Mac-Start.command should export NO_PROXY from resolve-no-proxy.mjs',
);
});
test('customer-facing .bat launchers are pure ASCII (cmd.exe mis-parses UTF-8 Chinese on GBK Windows)', () => {
// Non-ASCII bytes in a .bat get read as GBK by Chinese Windows cmd.exe, which
// garbles parsing ("usebackq is not a command"). Chinese UX must live in the
// node tools' stdout (rendered fine under chcp 65001), never in the .bat itself.
for (const name of [
'Windows-Start.bat',
'Windows-IntranetFix.bat',
'Windows-LocalModel.bat',
'OpenClaw-Doctor.bat',
]) {
const bytes = readFileSync(join(repoRoot, 'portable', name));
const offending = bytes.findIndex((b) => b > 0x7f);
assert.equal(offending, -1, `${name} has a non-ASCII byte at offset ${offending}`);
assert.ok(bytes.includes(0x0d), `${name} must use CRLF line endings`);
}
});
test('macOS .command launchers are LF-only (CRLF breaks #!/bin/bash on macOS)', () => {
for (const name of [
'Mac-Start.command',
'Mac-IntranetFix.command',
'Mac-LocalModel.command',
'Mac-OpenClaw-Doctor.command',
]) {
const bytes = readFileSync(join(repoRoot, 'portable', name));
const cr = bytes.indexOf(0x0d);
assert.equal(cr, -1, `${name} has a CR byte at offset ${cr} (must be LF-only)`);
assert.ok(bytes.toString('utf8').startsWith('#!/bin/bash'), `${name} must start with a clean shebang`);
}
});
test('macOS local-model / intranet launchers call the shared cross-platform scripts', () => {
assert.match(readRepoFile('portable', 'Mac-IntranetFix.command'), /lib\/intranet-check\.mjs/);
assert.match(readRepoFile('portable', 'Mac-LocalModel.command'), /lib\/setup-local-model\.mjs/);
assert.match(readRepoFile('portable', 'Mac-OpenClaw-Doctor.command'), /doctor --non-interactive/);
});
test('local-model setup launcher calls setup-local-model.mjs', () => {
const bat = readRepoFile('portable', 'Windows-LocalModel.bat');
assert.match(bat, /lib\\setup-local-model\.mjs/);
});
test('OpenClaw doctor launcher is read-only (no destructive repair flags)', () => {
const bat = readRepoFile('portable', 'OpenClaw-Doctor.bat');
assert.match(bat, /OPENCLAW_MJS%" doctor --non-interactive/);
// Must not auto-apply repairs that could overwrite user config/state.
assert.doesNotMatch(bat, /doctor[^\n]*--fix/);
assert.doesNotMatch(bat, /doctor[^\n]*--repair/);
assert.doesNotMatch(bat, /doctor[^\n]*--force/);
});
test('PowerShell installer generated start.bat disables OpenClaw bonjour discovery', () => {
const script = readRepoFile('install', 'install.ps1');
assert.match(script, /\$startBat = @'/);
assert.match(
script,
/set "OPENCLAW_DISABLE_BONJOUR=1"/,
'generated start.bat should disable bonjour discovery',
);
});
test('Electron desktop launcher disables OpenClaw bonjour discovery on Windows only', () => {
const source = readRepoFile('u-claw-app', 'src', 'main.js');
assert.match(
source,
/if\s*\(\s*process\.platform\s*===\s*['"]win32['"]\s*\)\s*{[\s\S]*?env\.OPENCLAW_DISABLE_BONJOUR\s*=\s*['"]1['"]/,
);
assert.doesNotMatch(
source,
/OPENCLAW_DISABLE_BONJOUR:\s*['"]1['"]/,
'bonjour disable flag should not be in the unconditional env object',
);
});