binary.test.js
1 'use strict' 2 /* eslint-disable @typescript-eslint/no-require-imports */ 3 4 const test = require('node:test') 5 const assert = require('node:assert/strict') 6 const fs = require('node:fs') 7 const os = require('node:os') 8 const path = require('node:path') 9 const { spawnSync } = require('node:child_process') 10 const { buildLegacyTsCliArgs } = require('../../bin/swarmclaw.js') 11 12 const CLI_BIN = path.join(__dirname, '..', '..', 'bin', 'swarmclaw.js') 13 const PACKAGE_JSON = require('../../package.json') 14 const APP_ROOT = path.join(__dirname, '..', '..') 15 16 function runBinary(args, options = {}) { 17 return spawnSync(process.execPath, [CLI_BIN, ...args], { 18 cwd: options.cwd || APP_ROOT, 19 env: { 20 ...process.env, 21 ...options.env, 22 }, 23 encoding: 'utf8', 24 }) 25 } 26 27 function runWithMockedFetch(args, options = {}) { 28 const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-binary-fetch-')) 29 const capturePath = path.join(tmpDir, 'capture.json') 30 const preloadPath = path.join(tmpDir, 'mock-fetch.cjs') 31 32 fs.writeFileSync( 33 preloadPath, 34 ` 35 const fs = require('node:fs') 36 globalThis.fetch = async (url, init = {}) => { 37 const capture = { 38 url: String(url), 39 method: init.method || 'GET', 40 headers: init.headers || {}, 41 body: typeof init.body === 'string' 42 ? init.body 43 : (Buffer.isBuffer(init.body) ? init.body.toString('utf8') : null), 44 } 45 fs.writeFileSync(process.env.SWARMCLAW_TEST_CAPTURE, JSON.stringify(capture), 'utf8') 46 return new Response(JSON.stringify([]), { 47 status: 200, 48 headers: { 'content-type': 'application/json' }, 49 }) 50 } 51 `, 52 'utf8', 53 ) 54 55 const nodeOptions = [process.env.NODE_OPTIONS, `--require=${preloadPath}`] 56 .filter(Boolean) 57 .join(' ') 58 59 const result = runBinary(args, { 60 ...options, 61 env: { 62 ...options.env, 63 NODE_OPTIONS: nodeOptions, 64 SWARMCLAW_TEST_CAPTURE: capturePath, 65 }, 66 }) 67 68 const capture = fs.existsSync(capturePath) 69 ? JSON.parse(fs.readFileSync(capturePath, 'utf8')) 70 : null 71 72 fs.rmSync(tmpDir, { recursive: true, force: true }) 73 return { result, capture } 74 } 75 76 test('legacy-routed binary commands honor SWARMCLAW_API_KEY', () => { 77 const { result, capture } = runWithMockedFetch( 78 ['runs', 'list', '--raw', '--url', 'http://localhost:3456'], 79 { 80 env: { 81 SWARMCLAW_API_KEY: 'legacy-api-key', 82 SWARMCLAW_ACCESS_KEY: '', 83 SC_ACCESS_KEY: '', 84 }, 85 }, 86 ) 87 88 assert.equal(result.status, 0, result.stderr) 89 assert.equal(result.stdout.trim(), '[]') 90 assert.equal(capture.headers['X-Access-Key'], 'legacy-api-key') 91 }) 92 93 test('legacy-routed binary commands fall back to platform-api-key.txt', () => { 94 const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-binary-keyfile-')) 95 fs.writeFileSync(path.join(tmpDir, 'platform-api-key.txt'), 'file-fallback-key\n', 'utf8') 96 97 const { result, capture } = runWithMockedFetch( 98 ['runs', 'list', '--raw', '--url', 'http://localhost:3456'], 99 { 100 cwd: tmpDir, 101 env: { 102 SWARMCLAW_API_KEY: '', 103 SWARMCLAW_ACCESS_KEY: '', 104 SC_ACCESS_KEY: '', 105 }, 106 }, 107 ) 108 109 assert.equal(result.status, 0, result.stderr) 110 assert.equal(result.stdout.trim(), '[]') 111 assert.equal(capture.headers['X-Access-Key'], 'file-fallback-key') 112 113 fs.rmSync(tmpDir, { recursive: true, force: true }) 114 }) 115 116 test('legacy-routed binary commands accept documented global aliases after the subcommand', () => { 117 const { result, capture } = runWithMockedFetch( 118 ['agents', 'list', '--access-key', 'alias-key', '--base-url', 'http://127.0.0.1:4567', '--json'], 119 ) 120 121 assert.equal(result.status, 0, result.stderr) 122 assert.equal(result.stdout.trim(), '[]') 123 assert.equal(capture.headers['X-Access-Key'], 'alias-key') 124 assert.equal(capture.url, 'http://127.0.0.1:4567/api/agents') 125 }) 126 127 test('binary server help exits successfully', () => { 128 const result = runBinary(['server', '--help']) 129 assert.equal(result.status, 0, result.stderr) 130 assert.match(result.stdout, /Usage: swarmclaw server/i) 131 }) 132 133 test('binary run alias routes to server help', () => { 134 const result = runBinary(['run', '--help']) 135 assert.equal(result.status, 0, result.stderr) 136 assert.match(result.stdout, /Usage: swarmclaw server/i) 137 }) 138 139 test('binary help command shows root help', () => { 140 const result = runBinary(['help']) 141 assert.equal(result.status, 0, result.stderr) 142 assert.match(result.stdout, /SwarmClaw CLI/i) 143 assert.match(result.stdout, /swarmclaw help \[command\]/i) 144 }) 145 146 test('binary help command shows command help for run alias', () => { 147 const result = runBinary(['help', 'run']) 148 assert.equal(result.status, 0, result.stderr) 149 assert.match(result.stdout, /Usage: swarmclaw server/i) 150 }) 151 152 test('binary help command shows command help for doctor alias', () => { 153 const result = runBinary(['help', 'doctor']) 154 assert.equal(result.status, 0, result.stderr) 155 assert.match(result.stdout, /Usage: swarmclaw doctor/i) 156 }) 157 158 test('binary status alias routes to local server status', () => { 159 const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-binary-status-')) 160 const result = runBinary(['status'], { 161 env: { 162 SWARMCLAW_HOME: homeDir, 163 }, 164 }) 165 166 assert.equal(result.status, 0, result.stderr) 167 assert.match(result.stdout, /Server: not running/i) 168 169 fs.rmSync(homeDir, { recursive: true, force: true }) 170 }) 171 172 test('binary doctor help exits successfully', () => { 173 const result = runBinary(['doctor', '--help']) 174 assert.equal(result.status, 0, result.stderr) 175 assert.match(result.stdout, /Usage: swarmclaw doctor/i) 176 }) 177 178 test('binary update help exits successfully', () => { 179 const result = runBinary(['update', '--help']) 180 assert.equal(result.status, 0, result.stderr) 181 assert.match(result.stdout, /Usage: swarmclaw update/i) 182 }) 183 184 test('binary version output matches package version', () => { 185 const result = runBinary(['--version']) 186 assert.equal(result.status, 0, result.stderr) 187 assert.equal(result.stdout.trim(), `${PACKAGE_JSON.name} ${PACKAGE_JSON.version}`) 188 }) 189 190 test('binary bare version alias output matches package version', () => { 191 const result = runBinary(['version']) 192 assert.equal(result.status, 0, result.stderr) 193 assert.equal(result.stdout.trim(), `${PACKAGE_JSON.name} ${PACKAGE_JSON.version}`) 194 }) 195 196 test('binary -v alias output matches package version', () => { 197 const result = runBinary(['-v']) 198 assert.equal(result.status, 0, result.stderr) 199 assert.equal(result.stdout.trim(), `${PACKAGE_JSON.name} ${PACKAGE_JSON.version}`) 200 }) 201 202 test('legacy TS launcher falls back to tsx import when strip-types is unavailable', () => { 203 const cliPath = path.join(APP_ROOT, 'src', 'cli', 'index.ts') 204 const args = buildLegacyTsCliArgs(cliPath, ['runs', 'list'], { 205 supportsStripTypes: false, 206 hasTsxRuntime: true, 207 }) 208 209 assert.deepEqual(args, ['--no-warnings', '--import', 'tsx', cliPath, 'runs', 'list']) 210 })