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 })