# Testing & Debugging — Functions

> Local development, debugging techniques, log inspection, and testing strategies for Aerostack functions.

Aerostack functions run on Cloudflare Workers, which means you get a full local development environment powered by the local runtime (Miniflare). All six platform bindings work locally — including Database, Cache, Queue, AI, Vector Search, and Storage.

---

## Local Development

### Start the dev server

```bash
aerostack dev
```

Starts a local development server with hot reload. All bindings are simulated locally. The function is accessible at `http://localhost:8787` by default.

### What happens locally

| Binding | Local behavior |
|---|---|
| `env.DB` | Local SQLite file (persistent between restarts) |
| `env.CACHE` | Local cache store (persistent between restarts) |
| `env.QUEUE` | Local queue simulation (messages delivered immediately) |
| `env.AI` | Requires `--remote` flag or mocked (see below) |
| `env.VECTORIZE` | Requires `--remote` flag or mocked |
| `env.STORAGE` | Local storage simulation (files stored in `.aerostack/state/`) |

  Database, Cache, and Storage work fully offline. AI and Vector Search require network access. Use `aerostack dev --remote` to test against real AI and Vector Search bindings, or mock them for unit tests.

### Testing with remote bindings

To test AI and Vector Search locally against Cloudflare's real infrastructure:

```bash
aerostack dev --remote
```

This runs your function locally but connects to production bindings. Use with caution — writes to the database, cache, and storage will affect real data.

### Local data persistence

Local data is stored in `.aerostack/state/` inside your project directory:

```
.aerostack/
  state/
    v3/
      d1/          # Local database files
      kv/          # Local cache data
      r2/          # Local storage objects
```

To reset local state and start fresh:

```bash
rm -rf .aerostack/state
aerostack dev
```

---

## Debugging

### Console logging

The simplest and most effective debugging tool. Logs appear in the terminal where `aerostack dev` is running.

```typescript

  async fetch(request: Request, env: Env): Promise {
    const url = new URL(request.url)
    console.log(`[${request.method}] ${url.pathname}`)

    const start = Date.now()
    const { results } = await env.DB.prepare('SELECT * FROM users').all()
    console.log(`DB query took ${Date.now() - start}ms, returned ${results.length} rows`)

    return Response.json(results)
  }
}
```

### Viewing logs in production

For deployed functions, use the Aerostack CLI to stream live logs:

```bash
aerostack logs my-function-name
```

This streams all `console.log`, `console.error`, and exception output from your production function in real-time. Filter by status:

```bash
# Only errors
aerostack logs my-function-name --status error

# Only specific paths
aerostack logs my-function-name --search "/api/users"
```

### Error handling pattern

Wrap your handler in a try-catch to ensure errors are always logged and returned as structured JSON:

```typescript

  async fetch(request: Request, env: Env): Promise {
    try {
      return await handleRequest(request, env)
    } catch (err) {
      console.error('Unhandled error:', err)

      const message = err instanceof Error ? err.message : 'Unknown error'
      const stack = err instanceof Error ? err.stack : undefined

      return Response.json({
        error: message,
        ...(env.ENVIRONMENT === 'development' ? { stack } : {})
      }, { status: 500 })
    }
  }
}

async function handleRequest(request: Request, env: Env): Promise {
  // Your actual logic here — errors bubble up to the catch block
}
```

---

## Testing Strategies

### Manual testing with curl

The fastest way to test during development:

```bash
# GET request
curl http://localhost:8787/api/users

# POST with JSON body
curl -X POST http://localhost:8787/api/users \
  -H "Content-Type: application/json" \
  -d '{"name": "Alice", "email": "alice@example.com"}'

# With headers
curl http://localhost:8787/api/users \
  -H "Authorization: Bearer test-token" \
  -H "Accept: application/json"

# See response headers
curl -i http://localhost:8787/api/users
```

### Unit testing with Vitest

Use Vitest with `@cloudflare/vitest-pool-workers` for unit tests that run against real Cloudflare bindings locally.

Install the test dependencies:

```bash
npm install -D vitest @cloudflare/vitest-pool-workers
```

Create `vitest.config.ts`:

```typescript

  test: {
    poolOptions: {
      workers: {
        wrangler: { configPath: './aerostack.toml' }
      }
    }
  }
})
```

Write a test in `test/index.test.ts`:

```typescript

describe('Bookmarks API', () => {
  beforeAll(async () => {
    // Set up test data
    await env.DB.exec(`
      CREATE TABLE IF NOT EXISTS bookmarks (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        url TEXT NOT NULL,
        title TEXT NOT NULL,
        created_at TEXT DEFAULT (datetime('now'))
      )
    `)
  })

  it('creates a bookmark', async () => {
    const request = new Request('http://localhost/bookmarks', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ url: 'https://example.com', title: 'Example' })
    })

    const ctx = createExecutionContext()
    const response = await worker.fetch(request, env, ctx)
    await waitOnExecutionContext(ctx)

    expect(response.status).toBe(201)

    const body = await response.json()
    expect(body.url).toBe('https://example.com')
    expect(body.title).toBe('Example')
    expect(body.id).toBeDefined()
  })

  it('lists bookmarks', async () => {
    const request = new Request('http://localhost/bookmarks')
    const ctx = createExecutionContext()
    const response = await worker.fetch(request, env, ctx)
    await waitOnExecutionContext(ctx)

    expect(response.status).toBe(200)

    const body = await response.json()
    expect(Array.isArray(body)).toBe(true)
    expect(body.length).toBeGreaterThan(0)
  })

  it('returns 400 for missing fields', async () => {
    const request = new Request('http://localhost/bookmarks', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ url: 'https://example.com' }) // missing title
    })

    const ctx = createExecutionContext()
    const response = await worker.fetch(request, env, ctx)
    await waitOnExecutionContext(ctx)

    expect(response.status).toBe(400)
  })
})
```

Run tests:

```bash
npx vitest run
```

### Integration testing

For functions that interact with multiple bindings, test the full flow:

```typescript
it('caches DB results', async () => {
  // First request — cache miss
  const res1 = await worker.fetch(new Request('http://localhost/bookmarks'), env, createExecutionContext())
  expect(res1.headers.get('X-Cache')).toBe('MISS')

  // Second request — cache hit
  const res2 = await worker.fetch(new Request('http://localhost/bookmarks'), env, createExecutionContext())
  expect(res2.headers.get('X-Cache')).toBe('HIT')

  // Verify same data
  const data1 = await res1.json()
  const data2 = await res2.json()
  expect(data1).toEqual(data2)
})
```

---

## Common Issues

### "No such table" error

Your database schema has not been applied locally. Run your migration:

```bash
aerostack db execute --local --file=schema.sql
```

### AI binding returns "AI is not defined"

The AI binding requires either:
- `--remote` flag when running `aerostack dev`
- The `[ai]` section in your `aerostack.toml`

```toml
[ai]
binding = "AI"
```

### Queue messages not being processed

Ensure your `aerostack.toml` has both a producer and consumer configuration:

```toml
[[queues.producers]]
binding = "QUEUE"
queue = "my-queue"

[[queues.consumers]]
queue = "my-queue"
```

And that your function exports a `queue()` handler.

### Cache returns null for a key that was just written

The cache is eventually consistent. In production, writes can take up to 60 seconds to propagate globally. For data that must be immediately consistent, use the database instead of the cache.

Locally, the cache is immediately consistent — this issue only appears in production.

### Function works locally but fails when deployed

Common causes:

1. **Missing secrets** — set them with `aerostack secrets set KEY`
2. **Schema not applied** — run migrations on the remote database
3. **Binding mismatch** — ensure `aerostack.toml` binding names match what your code uses (`env.DB`, `env.CACHE`, etc.)
4. **Compatibility date** — if using newer APIs, update `compatibility_date` in `aerostack.toml`
