/ src / cli / binary.test.js
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  })