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', ); });