/ src / lib / server / data-dir.test.ts
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  })