/ src / cli / server-cmd.test.js
server-cmd.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  
 10  function loadServerCmdForHome(homeDir) {
 11    const modPath = require.resolve('../../bin/server-cmd.js')
 12    const previousHome = process.env.SWARMCLAW_HOME
 13    process.env.SWARMCLAW_HOME = homeDir
 14    delete require.cache[modPath]
 15    const loaded = require(modPath)
 16    if (previousHome === undefined) delete process.env.SWARMCLAW_HOME
 17    else process.env.SWARMCLAW_HOME = previousHome
 18    delete require.cache[modPath]
 19    return loaded
 20  }
 21  
 22  test('needsBuild returns true when standalone output is missing from the package root', () => {
 23    const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-server-home-'))
 24    const pkgRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-server-pkg-'))
 25    const serverCmd = loadServerCmdForHome(homeDir)
 26  
 27    assert.equal(serverCmd.needsBuild(false, { pkgRoot }), true)
 28  
 29    fs.rmSync(homeDir, { recursive: true, force: true })
 30    fs.rmSync(pkgRoot, { recursive: true, force: true })
 31  })
 32  
 33  test('needsBuild returns false when standalone server exists in the package root', () => {
 34    const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-server-home-'))
 35    const pkgRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-server-pkg-'))
 36    const serverCmd = loadServerCmdForHome(homeDir)
 37  
 38    fs.mkdirSync(path.join(pkgRoot, '.next', 'standalone'), { recursive: true })
 39    fs.writeFileSync(path.join(pkgRoot, '.next', 'standalone', 'server.js'), 'console.log("ok")\n', 'utf8')
 40  
 41    assert.equal(serverCmd.needsBuild(false, { pkgRoot }), false)
 42  
 43    fs.rmSync(homeDir, { recursive: true, force: true })
 44    fs.rmSync(pkgRoot, { recursive: true, force: true })
 45  })
 46  
 47  test('findStandaloneServer recursively resolves nested standalone server paths', () => {
 48    const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-server-home-'))
 49    const pkgRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-server-pkg-'))
 50    const serverCmd = loadServerCmdForHome(homeDir)
 51  
 52    const nestedServer = path.join(pkgRoot, '.next', 'standalone', 'Users', 'wayde', 'Dev', 'swarmclaw', 'server.js')
 53    fs.mkdirSync(path.dirname(nestedServer), { recursive: true })
 54    fs.writeFileSync(nestedServer, 'console.log("ok")\n', 'utf8')
 55  
 56    assert.equal(serverCmd.findStandaloneServer({ pkgRoot }), nestedServer)
 57  
 58    fs.rmSync(homeDir, { recursive: true, force: true })
 59    fs.rmSync(pkgRoot, { recursive: true, force: true })
 60  })
 61  
 62  test('resolvePackageBuildRoot uses a versioned workspace for registry installs', () => {
 63    const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-server-home-'))
 64    const pkgRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-server-pkg-'))
 65    const serverCmd = loadServerCmdForHome(homeDir)
 66  
 67    fs.writeFileSync(
 68      path.join(pkgRoot, 'package.json'),
 69      JSON.stringify({ name: '@swarmclawai/swarmclaw', version: '1.0.2' }),
 70      'utf8',
 71    )
 72  
 73    assert.equal(
 74      serverCmd.resolvePackageBuildRoot(pkgRoot),
 75      path.join(homeDir, 'builds', 'package-1.0.2'),
 76    )
 77  
 78    fs.rmSync(homeDir, { recursive: true, force: true })
 79    fs.rmSync(pkgRoot, { recursive: true, force: true })
 80  })
 81  
 82  test('findStandaloneServer falls back to the external build workspace for registry installs', () => {
 83    const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-server-home-'))
 84    const pkgRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-server-pkg-'))
 85    const serverCmd = loadServerCmdForHome(homeDir)
 86  
 87    fs.writeFileSync(
 88      path.join(pkgRoot, 'package.json'),
 89      JSON.stringify({ name: '@swarmclawai/swarmclaw', version: '1.0.2' }),
 90      'utf8',
 91    )
 92  
 93    const nestedServer = path.join(
 94      serverCmd.resolvePackageBuildRoot(pkgRoot),
 95      '.next',
 96      'standalone',
 97      'Users',
 98      'wayde',
 99      'Dev',
100      'swarmclaw',
101      'server.js',
102    )
103    fs.mkdirSync(path.dirname(nestedServer), { recursive: true })
104    fs.writeFileSync(nestedServer, 'console.log("ok")\n', 'utf8')
105  
106    assert.equal(serverCmd.findStandaloneServer({ pkgRoot }), nestedServer)
107  
108    fs.rmSync(homeDir, { recursive: true, force: true })
109    fs.rmSync(pkgRoot, { recursive: true, force: true })
110  })
111  
112  test('prepareBuildWorkspace copies the package tree and links node_modules outside node_modules paths', () => {
113    const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-server-home-'))
114    const pkgRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-server-pkg-'))
115    const externalNodeModules = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-node-modules-'))
116    const serverCmd = loadServerCmdForHome(homeDir)
117  
118    fs.writeFileSync(
119      path.join(pkgRoot, 'package.json'),
120      JSON.stringify({ name: '@swarmclawai/swarmclaw', version: '1.0.2' }),
121      'utf8',
122    )
123    fs.mkdirSync(path.join(pkgRoot, 'src', 'app'), { recursive: true })
124    fs.writeFileSync(path.join(pkgRoot, 'src', 'app', 'page.tsx'), 'export default function Page() { return null }\n', 'utf8')
125  
126    const buildRoot = serverCmd.resolvePackageBuildRoot(pkgRoot)
127    serverCmd.prepareBuildWorkspace({ pkgRoot, buildRoot, nodeModulesDir: externalNodeModules })
128  
129    assert.equal(fs.readFileSync(path.join(buildRoot, 'package.json'), 'utf8'), fs.readFileSync(path.join(pkgRoot, 'package.json'), 'utf8'))
130    assert.equal(fs.readFileSync(path.join(buildRoot, 'src', 'app', 'page.tsx'), 'utf8'), 'export default function Page() { return null }\n')
131    assert.equal(fs.lstatSync(path.join(buildRoot, 'node_modules')).isSymbolicLink(), true)
132    assert.equal(fs.realpathSync(path.join(buildRoot, 'node_modules')), fs.realpathSync(externalNodeModules))
133  
134    fs.rmSync(homeDir, { recursive: true, force: true })
135    fs.rmSync(pkgRoot, { recursive: true, force: true })
136    fs.rmSync(externalNodeModules, { recursive: true, force: true })
137  })
138  
139  test('syncStandaloneRuntimeAssets copies .next/static and public into a direct standalone runtime', () => {
140    const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-server-home-'))
141    const pkgRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-server-pkg-'))
142    const serverCmd = loadServerCmdForHome(homeDir)
143    const runtimeDir = path.join(pkgRoot, '.next', 'standalone')
144  
145    fs.mkdirSync(path.join(pkgRoot, '.next', 'static', 'chunks'), { recursive: true })
146    fs.mkdirSync(path.join(pkgRoot, 'public', 'branding'), { recursive: true })
147    fs.writeFileSync(path.join(pkgRoot, '.next', 'static', 'chunks', 'app.js'), 'chunk\n', 'utf8')
148    fs.writeFileSync(path.join(pkgRoot, 'public', 'branding', 'logo.svg'), '<svg />\n', 'utf8')
149  
150    const result = serverCmd.syncStandaloneRuntimeAssets({
151      sourceRoot: pkgRoot,
152      runtimeDir,
153      force: true,
154    })
155  
156    assert.deepEqual(result, { staticCopied: true, publicCopied: true })
157    assert.equal(fs.readFileSync(path.join(runtimeDir, '.next', 'static', 'chunks', 'app.js'), 'utf8'), 'chunk\n')
158    assert.equal(fs.readFileSync(path.join(runtimeDir, 'public', 'branding', 'logo.svg'), 'utf8'), '<svg />\n')
159  
160    fs.rmSync(homeDir, { recursive: true, force: true })
161    fs.rmSync(pkgRoot, { recursive: true, force: true })
162  })
163  
164  test('syncStandaloneRuntimeAssets targets the resolved nested runtime directory', () => {
165    const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-server-home-'))
166    const pkgRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-server-pkg-'))
167    const serverCmd = loadServerCmdForHome(homeDir)
168    const serverJs = path.join(pkgRoot, '.next', 'standalone', 'Users', 'wayde', 'Dev', 'swarmclaw', 'server.js')
169    const runtimeDir = serverCmd.resolveStandaloneRuntimeDir(serverJs)
170  
171    fs.mkdirSync(path.dirname(serverJs), { recursive: true })
172    fs.writeFileSync(serverJs, 'console.log("ok")\n', 'utf8')
173    fs.mkdirSync(path.join(pkgRoot, '.next', 'static', 'css'), { recursive: true })
174    fs.writeFileSync(path.join(pkgRoot, '.next', 'static', 'css', 'app.css'), 'body{}\n', 'utf8')
175  
176    const result = serverCmd.syncStandaloneRuntimeAssets({
177      sourceRoot: pkgRoot,
178      runtimeDir,
179    })
180  
181    assert.deepEqual(result, { staticCopied: true, publicCopied: false })
182    assert.equal(fs.readFileSync(path.join(runtimeDir, '.next', 'static', 'css', 'app.css'), 'utf8'), 'body{}\n')
183    assert.equal(fs.existsSync(path.join(runtimeDir, 'public')), false)
184  
185    fs.rmSync(homeDir, { recursive: true, force: true })
186    fs.rmSync(pkgRoot, { recursive: true, force: true })
187  })
188  
189  test('syncStandaloneRuntimeAssets repairs missing assets without overwriting an existing target by default', () => {
190    const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-server-home-'))
191    const pkgRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-server-pkg-'))
192    const serverCmd = loadServerCmdForHome(homeDir)
193    const runtimeDir = path.join(pkgRoot, '.next', 'standalone')
194  
195    fs.mkdirSync(path.join(pkgRoot, '.next', 'static', 'chunks'), { recursive: true })
196    fs.writeFileSync(path.join(pkgRoot, '.next', 'static', 'chunks', 'main.js'), 'fresh\n', 'utf8')
197    fs.mkdirSync(path.join(runtimeDir, 'public'), { recursive: true })
198    fs.writeFileSync(path.join(runtimeDir, 'public', 'keep.txt'), 'keep\n', 'utf8')
199  
200    const result = serverCmd.syncStandaloneRuntimeAssets({
201      sourceRoot: pkgRoot,
202      runtimeDir,
203    })
204  
205    assert.deepEqual(result, { staticCopied: true, publicCopied: false })
206    assert.equal(fs.readFileSync(path.join(runtimeDir, '.next', 'static', 'chunks', 'main.js'), 'utf8'), 'fresh\n')
207    assert.equal(fs.readFileSync(path.join(runtimeDir, 'public', 'keep.txt'), 'utf8'), 'keep\n')
208  
209    fs.rmSync(homeDir, { recursive: true, force: true })
210    fs.rmSync(pkgRoot, { recursive: true, force: true })
211  })
212  
213  test('resolveReadyCheckHost maps wildcard bind hosts to loopback', () => {
214    const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-server-home-'))
215    const serverCmd = loadServerCmdForHome(homeDir)
216  
217    assert.equal(serverCmd.resolveReadyCheckHost('0.0.0.0'), '127.0.0.1')
218    assert.equal(serverCmd.resolveReadyCheckHost('::'), '::1')
219    assert.equal(serverCmd.resolveReadyCheckHost('127.0.0.1'), '127.0.0.1')
220  
221    fs.rmSync(homeDir, { recursive: true, force: true })
222  })
223  
224  test('waitForPortReady resolves once the readiness probe succeeds', async () => {
225    const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-server-home-'))
226    const serverCmd = loadServerCmdForHome(homeDir)
227    const calls = []
228    let attempts = 0
229  
230    await serverCmd.waitForPortReady({
231      host: '0.0.0.0',
232      port: 3456,
233      timeoutMs: 1_000,
234      intervalMs: 10,
235      probeFn: async (host, port) => {
236        calls.push({ host, port })
237        attempts += 1
238        return attempts >= 3
239      },
240    })
241  
242    assert.deepEqual(calls[0], { host: '127.0.0.1', port: 3456 })
243    assert.equal(calls.length, 3)
244    fs.rmSync(homeDir, { recursive: true, force: true })
245  })
246  
247  test('waitForPortReady fails fast when the detached process exits before readiness', async () => {
248    const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-server-home-'))
249    const serverCmd = loadServerCmdForHome(homeDir)
250  
251    await assert.rejects(
252      serverCmd.waitForPortReady({
253        host: '127.0.0.1',
254        port: 6553,
255        pid: 4242,
256        timeoutMs: 500,
257        intervalMs: 25,
258        isProcessRunningFn: () => false,
259      }),
260      /exited before becoming ready/,
261    )
262  
263    fs.rmSync(homeDir, { recursive: true, force: true })
264  })