data-dir.test.ts
1 import assert from 'node:assert/strict' 2 import fs from 'node:fs' 3 import os from 'node:os' 4 import path from 'node:path' 5 import { spawnSync } from 'node:child_process' 6 import { describe, it } from 'node:test' 7 8 const repoRoot = path.resolve(path.dirname(new URL(import.meta.url).pathname), '../../..') 9 10 function extractLastJson(stdout: string): Record<string, unknown> { 11 const lines = stdout 12 .trim() 13 .split('\n') 14 .map((line) => line.trim()) 15 .filter(Boolean) 16 const jsonLine = [...lines].reverse().find((line) => line.startsWith('{')) 17 return JSON.parse(jsonLine || '{}') 18 } 19 20 describe('data-dir resolution', () => { 21 it('falls back to in-project workspace when the external workspace root exists but child writes fail', () => { 22 const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-data-dir-')) 23 const fakeHome = path.join(tempDir, 'home') 24 const dataDir = path.join(tempDir, 'data') 25 const externalWorkspace = path.join(fakeHome, '.swarmclaw', 'workspace') 26 fs.mkdirSync(externalWorkspace, { recursive: true }) 27 fs.chmodSync(externalWorkspace, 0o555) 28 29 try { 30 const result = spawnSync(process.execPath, ['--import', 'tsx', '--input-type=module', '--eval', ` 31 const modNs = await import('./src/lib/server/data-dir') 32 const mod = modNs.default || modNs['module.exports'] || modNs 33 console.log(JSON.stringify({ 34 dataDir: mod.DATA_DIR, 35 workspaceDir: mod.WORKSPACE_DIR, 36 })) 37 `], { 38 cwd: repoRoot, 39 env: { 40 ...process.env, 41 HOME: fakeHome, 42 DATA_DIR: dataDir, 43 }, 44 encoding: 'utf-8', 45 }) 46 47 assert.equal(result.status, 0, result.stderr || result.stdout || 'subprocess failed') 48 const payload = extractLastJson(result.stdout || '') 49 assert.equal(payload.dataDir, dataDir) 50 assert.equal(payload.workspaceDir, path.join(dataDir, 'workspace')) 51 } finally { 52 fs.chmodSync(externalWorkspace, 0o755) 53 fs.rmSync(tempDir, { recursive: true, force: true }) 54 } 55 }) 56 57 it('uses isolated temp dirs during build bootstrap when DATA_DIR is unset', () => { 58 const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-data-dir-build-')) 59 const fakeHome = path.join(tempDir, 'home') 60 61 try { 62 const env = { ...process.env, HOME: fakeHome, npm_lifecycle_event: 'build:ci' } as NodeJS.ProcessEnv 63 delete (env as Record<string, unknown>).DATA_DIR 64 delete (env as Record<string, unknown>).WORKSPACE_DIR 65 delete (env as Record<string, unknown>).BROWSER_PROFILES_DIR 66 67 const result = spawnSync(process.execPath, ['--import', 'tsx', '--input-type=module', '--eval', ` 68 const modNs = await import('./src/lib/server/data-dir') 69 const mod = modNs.default || modNs['module.exports'] || modNs 70 console.log(JSON.stringify({ 71 isBuildBootstrap: mod.IS_BUILD_BOOTSTRAP, 72 dataDir: mod.DATA_DIR, 73 workspaceDir: mod.WORKSPACE_DIR, 74 browserProfilesDir: mod.BROWSER_PROFILES_DIR, 75 })) 76 `], { 77 cwd: repoRoot, 78 env, 79 encoding: 'utf-8', 80 }) 81 82 assert.equal(result.status, 0, result.stderr || result.stdout || 'subprocess failed') 83 const payload = extractLastJson(result.stdout || '') 84 const expectedDataDir = path.join(os.tmpdir(), 'swarmclaw-build-data') 85 assert.equal(payload.isBuildBootstrap, true) 86 assert.equal(payload.dataDir, expectedDataDir) 87 assert.equal(payload.workspaceDir, path.join(expectedDataDir, 'workspace')) 88 assert.equal(payload.browserProfilesDir, path.join(expectedDataDir, 'browser-profiles')) 89 } finally { 90 fs.rmSync(tempDir, { recursive: true, force: true }) 91 } 92 }) 93 94 it('derives runtime directories from SWARMCLAW_HOME when set', () => { 95 const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-data-dir-home-')) 96 const fakeHome = path.join(tempDir, 'home') 97 const swarmclawHome = path.join(tempDir, 'project', '.swarmclaw') 98 99 try { 100 const env = { ...process.env, HOME: fakeHome, SWARMCLAW_HOME: swarmclawHome } as NodeJS.ProcessEnv 101 delete (env as Record<string, unknown>).DATA_DIR 102 delete (env as Record<string, unknown>).WORKSPACE_DIR 103 delete (env as Record<string, unknown>).BROWSER_PROFILES_DIR 104 105 const result = spawnSync(process.execPath, ['--import', 'tsx', '--input-type=module', '--eval', ` 106 const modNs = await import('./src/lib/server/data-dir') 107 const mod = modNs.default || modNs['module.exports'] || modNs 108 console.log(JSON.stringify({ 109 dataDir: mod.DATA_DIR, 110 workspaceDir: mod.WORKSPACE_DIR, 111 browserProfilesDir: mod.BROWSER_PROFILES_DIR, 112 })) 113 `], { 114 cwd: repoRoot, 115 env, 116 encoding: 'utf-8', 117 }) 118 119 assert.equal(result.status, 0, result.stderr || result.stdout || 'subprocess failed') 120 const payload = extractLastJson(result.stdout || '') 121 assert.equal(payload.dataDir, path.join(swarmclawHome, 'data')) 122 assert.equal(payload.workspaceDir, path.join(swarmclawHome, 'workspace')) 123 assert.equal(payload.browserProfilesDir, path.join(swarmclawHome, 'browser-profiles')) 124 } finally { 125 fs.rmSync(tempDir, { recursive: true, force: true }) 126 } 127 }) 128 })