/ src / lib / server / runtime / estop.test.ts
estop.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, beforeEach, describe, it } from 'node:test'
 6  
 7  const originalEnv = {
 8    DATA_DIR: process.env.DATA_DIR,
 9    WORKSPACE_DIR: process.env.WORKSPACE_DIR,
10  }
11  
12  let tempDir = ''
13  let storage: typeof import('@/lib/server/storage')
14  let approvals: typeof import('@/lib/server/approvals')
15  let estop: typeof import('@/lib/server/runtime/estop')
16  
17  function resetEstopState() {
18    estop.saveEstopState({
19      level: 'none',
20      reason: null,
21      engagedAt: null,
22      engagedBy: null,
23      resumeApprovalId: null,
24      updatedAt: Date.now(),
25    })
26  }
27  
28  before(async () => {
29    tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-estop-'))
30    process.env.DATA_DIR = path.join(tempDir, 'data')
31    process.env.WORKSPACE_DIR = path.join(tempDir, 'workspace')
32  
33    storage = await import('@/lib/server/storage')
34    approvals = await import('@/lib/server/approvals')
35    estop = await import('@/lib/server/runtime/estop')
36  })
37  
38  beforeEach(() => {
39    storage.saveSettings({})
40    for (const id of Object.keys(storage.loadApprovals())) {
41      storage.deleteApproval(id)
42    }
43    resetEstopState()
44  })
45  
46  after(() => {
47    for (const [key, value] of Object.entries(originalEnv)) {
48      if (value === undefined) delete process.env[key]
49      else process.env[key] = value
50    }
51    fs.rmSync(tempDir, { recursive: true, force: true })
52  })
53  
54  describe('estop resume approvals', () => {
55    it('defaults to direct resume when no setting is persisted', () => {
56      estop.engageEstop({ level: 'autonomy', engagedBy: 'test' })
57  
58      assert.equal(estop.areEstopResumeApprovalsEnabled(), false)
59  
60      const resumed = estop.resumeEstop({ bypassApproval: true })
61      assert.equal(resumed.level, 'none')
62      assert.equal(resumed.resumeApprovalId, null)
63    })
64  
65    it('requires an approved human-loop decision when the policy is enabled', async () => {
66      storage.saveSettings({ autonomyResumeApprovalsEnabled: true })
67      estop.engageEstop({ level: 'all', engagedBy: 'test' })
68  
69      assert.equal(estop.areEstopResumeApprovalsEnabled(), true)
70  
71      const request = estop.requestEstopResumeApproval({ requester: 'test' })
72      assert.ok(request.approval)
73      assert.equal(request.state.resumeApprovalId, request.approval?.id)
74  
75      assert.throws(() => estop.resumeEstop({ approvalId: request.approval?.id }), /not approved yet/i)
76  
77      await approvals.submitDecision(request.approval!.id, true)
78      const resumed = estop.resumeEstop({ approvalId: request.approval!.id })
79  
80      assert.equal(resumed.level, 'none')
81      assert.equal(resumed.resumeApprovalId, request.approval!.id)
82    })
83  
84    it('retires pending estop approvals when the operator bypasses them', () => {
85      storage.saveSettings({ autonomyResumeApprovalsEnabled: true })
86      const engaged = estop.engageEstop({ level: 'autonomy', engagedBy: 'test' })
87      const request = estop.requestEstopResumeApproval({ requester: 'test' })
88  
89      assert.equal(engaged.level, 'autonomy')
90      assert.equal(request.approval?.status, 'pending')
91  
92      const resumed = estop.resumeEstop({ bypassApproval: true })
93      const clearedApproval = estop.findEstopResumeApproval(request.approval!.id)
94  
95      assert.equal(resumed.level, 'none')
96      assert.equal(clearedApproval?.status, 'rejected')
97    })
98  })