/ src / lib / server / runtime / runtime-settings.test.ts
runtime-settings.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 runWithTempDataDir(script: string) {
 11    const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-runtime-settings-'))
 12    try {
 13      const result = spawnSync(process.execPath, ['--import', 'tsx', '--input-type=module', '--eval', script], {
 14        cwd: repoRoot,
 15        env: {
 16          ...process.env,
 17          DATA_DIR: tempDir,
 18          WORKSPACE_DIR: path.join(tempDir, 'workspace'),
 19        },
 20        encoding: 'utf-8',
 21      })
 22      assert.equal(result.status, 0, result.stderr || result.stdout || 'subprocess failed')
 23      const lines = (result.stdout || '')
 24        .trim()
 25        .split('\n')
 26        .map((line) => line.trim())
 27        .filter(Boolean)
 28      const jsonLine = [...lines].reverse().find((line) => line.startsWith('{'))
 29      return JSON.parse(jsonLine || '{}')
 30    } finally {
 31      fs.rmSync(tempDir, { recursive: true, force: true })
 32    }
 33  }
 34  
 35  describe('runtime settings defaults', () => {
 36    it('backfills explicit runtime defaults for clean installs', () => {
 37      const output = runWithTempDataDir(`
 38        const storageMod = await import('@/lib/server/storage')
 39        const runtimeMod = await import('@/lib/server/runtime/runtime-settings')
 40        const storage = storageMod.default || storageMod
 41        const runtime = runtimeMod.default || runtimeMod
 42        console.log(JSON.stringify({
 43          settings: storage.loadSettings(),
 44          runtime: runtime.loadRuntimeSettings(),
 45        }))
 46      `)
 47  
 48      assert.equal(output.settings.loopMode, 'bounded')
 49      assert.equal(output.settings.agentLoopRecursionLimit, 300)
 50      assert.equal(output.settings.ongoingLoopMaxIterations, 250)
 51      assert.equal(output.settings.ongoingLoopMaxRuntimeMinutes, 60)
 52      assert.equal(output.settings.delegationMaxDepth, 3)
 53      assert.equal(output.settings.shellCommandTimeoutSec, 120)
 54      assert.equal(output.settings.claudeCodeTimeoutSec, 1800)
 55      assert.equal(output.settings.cliProcessTimeoutSec, 1800)
 56      assert.equal(output.settings.heartbeatIntervalSec, 1800)
 57      assert.equal(output.settings.heartbeatAckMaxChars, 300)
 58      assert.equal(output.settings.heartbeatShowOk, false)
 59      assert.equal(output.settings.heartbeatShowAlerts, true)
 60      assert.equal(output.settings.heartbeatTarget, null)
 61      assert.equal(output.settings.heartbeatPrompt, null)
 62      assert.equal(output.runtime.agentLoopRecursionLimit, 300)
 63    })
 64  
 65    it('clamps invalid persisted runtime settings into the supported range', () => {
 66      const output = runWithTempDataDir(`
 67        const storageMod = await import('@/lib/server/storage')
 68        const runtimeMod = await import('@/lib/server/runtime/runtime-settings')
 69        const storage = storageMod.default || storageMod
 70        const runtime = runtimeMod.default || runtimeMod
 71  
 72        storage.saveSettings({
 73          loopMode: 'invalid',
 74          agentLoopRecursionLimit: 999,
 75          ongoingLoopMaxIterations: 999999,
 76          ongoingLoopMaxRuntimeMinutes: -1,
 77          delegationMaxDepth: 99,
 78          shellCommandTimeoutSec: 0,
 79          claudeCodeTimeoutSec: 999999,
 80          cliProcessTimeoutSec: 'abc',
 81          heartbeatIntervalSec: 999999,
 82          heartbeatAckMaxChars: -50,
 83          heartbeatShowOk: 'yes',
 84          heartbeatShowAlerts: 'off',
 85          heartbeatTarget: '   ',
 86          heartbeatPrompt: '   ',
 87        })
 88  
 89        console.log(JSON.stringify({
 90          settings: storage.loadSettings(),
 91          runtime: runtime.loadRuntimeSettings(),
 92        }))
 93      `)
 94  
 95      assert.equal(output.settings.loopMode, 'bounded')
 96      assert.equal(output.settings.agentLoopRecursionLimit, 500)
 97      assert.equal(output.settings.ongoingLoopMaxIterations, 5000)
 98      assert.equal(output.settings.ongoingLoopMaxRuntimeMinutes, 0)
 99      assert.equal(output.settings.delegationMaxDepth, 12)
100      assert.equal(output.settings.shellCommandTimeoutSec, 1)
101      assert.equal(output.settings.claudeCodeTimeoutSec, 7200)
102      assert.equal(output.settings.cliProcessTimeoutSec, 1800)
103      assert.equal(output.settings.heartbeatIntervalSec, 86400)
104      assert.equal(output.settings.heartbeatAckMaxChars, 0)
105      assert.equal(output.settings.heartbeatShowOk, true)
106      assert.equal(output.settings.heartbeatShowAlerts, false)
107      assert.equal(output.settings.heartbeatTarget, null)
108      assert.equal(output.settings.heartbeatPrompt, null)
109      assert.equal(output.runtime.ongoingLoopMaxRuntimeMs, null)
110    })
111  })