route.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 test 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-approvals-route-')) 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: path.join(tempDir, 'data'), 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 test('GET and POST /api/approvals handle human-loop approvals only', () => { 36 const output = runWithTempDataDir(` 37 const storageMod = await import('./src/lib/server/storage') 38 const approvalsMod = await import('./src/lib/server/approvals') 39 const routeMod = await import('./src/app/api/approvals/route') 40 const storage = storageMod.default || storageMod 41 const approvals = approvalsMod.default || approvalsMod 42 const route = routeMod.default || routeMod 43 44 const humanApproval = approvals.requestApproval({ 45 category: 'human_loop', 46 title: 'Approve deploy', 47 data: { question: 'Deploy now?' }, 48 }) 49 50 const pendingBefore = await route.GET(new Request('http://local/api/approvals')) 51 const pendingBeforeJson = await pendingBefore.json() 52 53 const approveResponse = await route.POST(new Request('http://local/api/approvals', { 54 method: 'POST', 55 headers: { 'content-type': 'application/json' }, 56 body: JSON.stringify({ id: humanApproval.id, approved: true }), 57 })) 58 const approvePayload = await approveResponse.json() 59 60 const pendingAfter = await route.GET(new Request('http://local/api/approvals')) 61 const pendingAfterJson = await pendingAfter.json() 62 63 const storedApproval = storage.loadApprovals()[humanApproval.id] 64 console.log(JSON.stringify({ 65 pendingBeforeCount: Array.isArray(pendingBeforeJson) ? pendingBeforeJson.length : -1, 66 pendingBeforeId: Array.isArray(pendingBeforeJson) ? pendingBeforeJson[0]?.id || null : null, 67 approveStatus: approveResponse.status, 68 approvePayload, 69 pendingAfterCount: Array.isArray(pendingAfterJson) ? pendingAfterJson.length : -1, 70 storedStatus: storedApproval?.status || null, 71 })) 72 `) 73 74 assert.equal(output.pendingBeforeCount, 1) 75 assert.notEqual(output.pendingBeforeId, null) 76 assert.equal(output.approveStatus, 200) 77 assert.equal(output.approvePayload?.ok, true) 78 assert.equal(output.pendingAfterCount, 0) 79 assert.equal(output.storedStatus, 'approved') 80 }) 81 82 test('POST /api/approvals rejects invalid payloads', () => { 83 const output = runWithTempDataDir(` 84 const approvalsMod = await import('./src/lib/server/approvals') 85 const routeMod = await import('./src/app/api/approvals/route') 86 const approvals = approvalsMod.default || approvalsMod 87 const route = routeMod.default || routeMod 88 89 const humanApproval = approvals.requestApproval({ 90 category: 'human_loop', 91 title: 'Test approval', 92 data: { question: 'Proceed?' }, 93 }) 94 95 const invalidResponse = await route.POST(new Request('http://local/api/approvals', { 96 method: 'POST', 97 headers: { 'content-type': 'application/json' }, 98 body: JSON.stringify({ id: humanApproval.id }), 99 })) 100 const invalidPayload = await invalidResponse.json() 101 102 console.log(JSON.stringify({ 103 invalidStatus: invalidResponse.status, 104 invalidError: invalidPayload?.error || null, 105 invalidIssues: invalidPayload?.issues || null, 106 })) 107 `) 108 109 assert.equal(output.invalidStatus, 400) 110 assert.equal(output.invalidError, 'Validation failed') 111 assert.deepEqual(output.invalidIssues, [ 112 { path: 'approved', message: 'Invalid input: expected boolean, received undefined' }, 113 ]) 114 }) 115 116 test('POST /api/approvals rejects malformed JSON with a 400', () => { 117 const output = runWithTempDataDir(` 118 const routeMod = await import('./src/app/api/approvals/route') 119 const route = routeMod.default || routeMod 120 121 const invalidResponse = await route.POST(new Request('http://local/api/approvals', { 122 method: 'POST', 123 headers: { 'content-type': 'application/json' }, 124 body: '{bad-json', 125 })) 126 const invalidPayload = await invalidResponse.json() 127 128 console.log(JSON.stringify({ 129 invalidStatus: invalidResponse.status, 130 invalidError: invalidPayload?.error || null, 131 })) 132 `) 133 134 assert.equal(output.invalidStatus, 400) 135 assert.equal(output.invalidError, 'Invalid or missing request body') 136 })