/ src / app / api / approvals / route.test.ts
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  })