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 })