/ src / lib / server / approvals.test.ts
approvals.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 { after, before, describe, it } from 'node:test'
  6  
  7  const originalEnv = {
  8    DATA_DIR: process.env.DATA_DIR,
  9    WORKSPACE_DIR: process.env.WORKSPACE_DIR,
 10    SWARMCLAW_BUILD_MODE: process.env.SWARMCLAW_BUILD_MODE,
 11  }
 12  
 13  let tempDir = ''
 14  let approvals: typeof import('./approvals')
 15  
 16  before(async () => {
 17    tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-approvals-'))
 18    process.env.DATA_DIR = path.join(tempDir, 'data')
 19    process.env.WORKSPACE_DIR = path.join(tempDir, 'workspace')
 20    process.env.SWARMCLAW_BUILD_MODE = '1'
 21    approvals = await import('./approvals')
 22  })
 23  
 24  after(() => {
 25    if (originalEnv.DATA_DIR === undefined) delete process.env.DATA_DIR
 26    else process.env.DATA_DIR = originalEnv.DATA_DIR
 27    if (originalEnv.WORKSPACE_DIR === undefined) delete process.env.WORKSPACE_DIR
 28    else process.env.WORKSPACE_DIR = originalEnv.WORKSPACE_DIR
 29    if (originalEnv.SWARMCLAW_BUILD_MODE === undefined) delete process.env.SWARMCLAW_BUILD_MODE
 30    else process.env.SWARMCLAW_BUILD_MODE = originalEnv.SWARMCLAW_BUILD_MODE
 31    fs.rmSync(tempDir, { recursive: true, force: true })
 32  })
 33  
 34  describe('approvals', () => {
 35    it('creates a pending approval with correct fields', () => {
 36      const result = approvals.requestApproval({
 37        category: 'human_loop',
 38        title: 'Confirm deployment',
 39        data: { question: 'Deploy to prod?' },
 40        agentId: 'agent-1',
 41        sessionId: 'session-1',
 42      })
 43  
 44      assert.equal(result.status, 'pending')
 45      assert.equal(result.category, 'human_loop')
 46      assert.equal(result.agentId, 'agent-1')
 47      assert.equal(result.sessionId, 'session-1')
 48      assert.ok(result.id.length > 0)
 49      assert.ok(result.createdAt > 0)
 50      assert.equal(result.createdAt, result.updatedAt)
 51    })
 52  
 53    it('lists only pending approvals and can filter by category', async () => {
 54      const human = approvals.requestApproval({
 55        category: 'human_loop',
 56        title: 'Human approval',
 57        data: { question: 'Proceed?' },
 58      })
 59      const other = approvals.requestApproval({
 60        category: 'human_loop',
 61        title: 'Another approval',
 62        data: { question: 'Continue?' },
 63      })
 64  
 65      await approvals.submitDecision(other.id, true)
 66  
 67      const pending = approvals.listPendingApprovals()
 68      const humanPending = approvals.listPendingApprovals('human_loop')
 69  
 70      assert.equal(pending.some((entry) => entry.id === human.id), true)
 71      assert.equal(pending.some((entry) => entry.id === other.id), false)
 72      assert.equal(humanPending.some((entry) => entry.id === human.id), true)
 73      assert.equal(humanPending.every((entry) => entry.category === 'human_loop'), true)
 74    })
 75  
 76    it('approves a pending request', async () => {
 77      const req = approvals.requestApproval({
 78        category: 'human_loop',
 79        title: 'Confirm deployment',
 80        data: { question: 'Deploy to prod?' },
 81        sessionId: null,
 82        agentId: null,
 83      })
 84  
 85      const updated = await approvals.submitDecision(req.id, true)
 86  
 87      assert.equal(updated.status, 'approved')
 88      assert.equal(approvals.listPendingApprovals().some((entry) => entry.id === req.id), false)
 89    })
 90  
 91    it('rejects a pending request', async () => {
 92      const req = approvals.requestApproval({
 93        category: 'human_loop',
 94        title: 'Confirm deletion',
 95        data: { question: 'Delete everything?' },
 96        sessionId: null,
 97        agentId: null,
 98      })
 99  
100      const updated = await approvals.submitDecision(req.id, false)
101  
102      assert.equal(updated.status, 'rejected')
103      assert.equal(approvals.listPendingApprovals().some((entry) => entry.id === req.id), false)
104    })
105  
106    it('throws for non-existent approval id', async () => {
107      await assert.rejects(
108        () => approvals.submitDecision('nonexistent-xyz', true),
109        /not found/i,
110      )
111    })
112  
113    it('is idempotent for repeated decisions', async () => {
114      const req = approvals.requestApproval({
115        category: 'human_loop',
116        title: 'Idempotent test',
117        data: { question: 'yes?' },
118      })
119  
120      const approved = await approvals.submitDecision(req.id, true)
121      const repeated = await approvals.submitDecision(req.id, true)
122  
123      assert.equal(approved.status, 'approved')
124      assert.equal(repeated.status, 'approved')
125    })
126  })