> ## Documentation Index
> Fetch the complete documentation index at: https://sdk.ekanos.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Testing

> Write unit tests for handlers using createMockContext, invokeWebhook, invokeSchedule, and invokeActivate.

`@ekanos/sdk/testing` gives you the same context production builds get, in memory: same schema validation, same egress allowlist, same secret tier rules, no network.

## Create a mock context

```ts theme={null}
import { createMockContext } from '@ekanos/sdk/testing';

const ctx = createMockContext({
  integration: { slug: 'acme-payments' },
  storageSchemas: myStorage,
  egress: integration.egress,
  storage: {
    account: {
      settings: { merchantId: 'mrc_4820193' },
    },
  },
  secrets: {
    account: {
      acme_api_key: 'acme_test_9f2c41ab', // Writable
    },
    admin: {
      platform_key: 'not-writable', // Throws on write
    },
  },
  fetchHandlers: [
    {
      match: 'https://api.acme.example', // String matches URLs that start with it
      respond: () => Response.json({ payments: [] }),
    },
    {
      match: /^https:\/\/api\.acme\.example\/v[0-9]+\/.*/, // RegExp tested against full URL
      respond: (request) => Response.json({ url: request.url }), // receives a Request
    },
  ],
});

const result = await integration.tools[0].run(ctx, {});

// Handlers are tried in order and the first match wins. An allowed call
// that matches nothing resolves 200 {}.

// Query what happened
expect(ctx.fetchCalls).toHaveLength(1); // { url, init?, denied }[]
expect(ctx.logs.at(-1)?.level).toBe('info'); // { level, context, message }[]
expect(ctx.dumpStorage().account).toMatchObject({/* … */});
```

## Test tools

```ts theme={null}
import { createMockContext } from '@ekanos/sdk/testing';

const ctx = createMockContext({
  integration: { slug: 'acme-payments' },
  storageSchemas: myStorage,
  egress: ['https://api.acme.example'],
  secrets: { account: { api_key: 'test_key' } },
  fetchHandlers: [
    {
      match: 'https://api.acme.example',
      respond: () => Response.json({ temperature: 12.4 }),
    },
  ],
});

const result = await integration.tools[0].run(ctx, { units: 'celsius' });

expect(result).toEqual({ temperature: 12.4 });
expect(ctx.logs).toHaveLength(0); // No errors logged
```

## Test webhooks

```ts theme={null}
import { invokeWebhook } from '@ekanos/sdk/testing';

const { result, event, ctx } = await invokeWebhook(
  integration,
  'payment-updated',
  {
    eventType: 'payment.updated',
    payment: { id: 'pay_123', status: 'succeeded' },
    previousStatus: 'pending',
  },
  {
    contextOptions: {
      storage: { account: { settings: { merchantId: 'mrc_123' } } },
      fetchHandlers: [/* … */],
    },
    headers: { 'x-acme-signature': 'signature_value' },
  },
);

expect(result.status).toBe('processed');
expect(ctx.dumpStorage().account).toMatchObject({/* … */});
```

`invokeWebhook` builds the context from the definition — its slug, storage schemas, and egress — so `contextOptions` cannot override those. It throws if the webhook id is not declared or the payload fails `payloadSchema`; the handler never runs. Signatures are never verified locally: for a signed webhook, the skip is recorded as one `warn` line on `ctx.logs`.

## Test schedules

```ts theme={null}
import { invokeSchedule } from '@ekanos/sdk/testing';

const { result, ctx } = await invokeSchedule(integration, 'daily-reconcile', {
  contextOptions: {
    secrets: { account: { api_key: 'test_key' } },
    fetchHandlers: [
      { match: 'https://api.acme.example', respond: () => Response.json([]) },
    ],
  },
  trigger: 'schedule', // defaults to 'manual'
});

expect(result.status).toBe('completed');

// Reuse context across invocations to test accumulation
const second = await invokeSchedule(integration, 'daily-reconcile', {
  context: ctx, // State carries over
});
```

Pass `context` OR `contextOptions`, never both — passing both throws a `TypeError`. `invokeSchedule` throws if the schedule id is not declared.

## Test activation hooks

```ts theme={null}
import { invokeActivate } from '@ekanos/sdk/testing';

const { ctx } = await invokeActivate(integration, {
  contextOptions: {
    secrets: { account: { acme_api_key: 'test_key' } },
    fetchHandlers: [
      { match: 'https://api.acme.example', respond: () => Response.json({}) },
    ],
  },
});

expect(ctx.dumpStorage().account['cache/summary']).toBeDefined();
```

Throws if `onActivate` is not declared, or if the handler itself throws. `invokeActivate` is a bare invoker, not the host — the non-fatal (log-and-continue) handling is applied by whatever calls the hook in production or in the harness, so a test of that behavior should catch the rejection itself.

## Common patterns

### Test with multiple assertions

```ts theme={null}
const ctx = createMockContext(/* … */);

// First run
let result = await integration.schedules[0].handler(ctx, {/* … */});
expect(result.status).toBe('completed');
expect(ctx.dumpStorage().account.cache).toBeDefined();

// Second run — context carries over, state accumulates
result = await integration.schedules[0].handler(ctx, {/* … */});
expect(result.status).toBe('completed'); // Should be idempotent
```

### Test error handling

```ts theme={null}
const ctx = createMockContext({
  // …
  fetchHandlers: [
    {
      match: 'https://api.acme.example',
      respond: () => Response.json({}, { status: 500 }), // Simulate error
    },
  ],
});

const result = await integration.tools[0].run(ctx, {});
expect(result.error).toContain('500');
expect(ctx.logs.some((l) => l.level === 'error')).toBe(true);
```

### Test egress denial

```ts theme={null}
import { EgressDeniedError } from '@ekanos/sdk/context';

const ctx = createMockContext({
  integration: { slug: 'acme-payments' },
  egress: ['https://allowed.example'], // Only this origin
});

// ctx.fetch throws EgressDeniedError synchronously; inside an async run()
// that surfaces as a rejection unless your handler catches it.
await expect(integration.tools[0].run(ctx, {})).rejects.toBeInstanceOf(
  EgressDeniedError,
);
expect(ctx.fetchCalls.at(-1)?.denied).toBe(true);
```

### Test storage validation

```ts theme={null}
import { StorageValidationError } from '@ekanos/sdk/context';

// Seeds are validated through the write path, so a bad seed throws
// StorageValidationError from createMockContext itself.
expect(() =>
  createMockContext({
    integration: { slug: 'acme-payments' },
    storageSchemas: myStorage,
    storage: { account: { settings: { wrongField: 123 } } },
  }),
).toThrow(StorageValidationError);

// A write that fails the schema rejects.
await expect(
  ctx.storage.account.set('settings', { wrongField: 123 }),
).rejects.toBeInstanceOf(StorageValidationError);
```

## Record fetch calls

```ts theme={null}
const ctx = createMockContext(/* … */);
await integration.tools[0].run(ctx, {});

ctx.fetchCalls.forEach((call) => {
  console.log(call.url, call.init, call.denied);
});
```

Each call is `{ url, init?, denied: boolean }`, in order, allowed and denied alike.

## Record logs

```ts theme={null}
const ctx = createMockContext(/* … */);
await integration.tools[0].run(ctx, {});

ctx.logs.forEach((log) => {
  console.log(log.level, log.context, log.message);
});
```

Each log is `{ level: 'debug' | 'info' | 'warn' | 'error', context, message }`.

## Testing without the harness

You do not need `@ekanos/cli` or `@ekanos/harness` to test handlers, webhooks, schedules, or activation hooks. Everything in this section works with `vitest` and `createMockContext()` alone.

For widget tests that call `useIntegrationFetch()`, `createMockFetchProvider(handlers)` returns a provider component that answers from the same `MockFetchHandler` fixtures (no egress check). Seeing widgets, tiles, and forms rendered in Ekanos chrome takes the harness.

## Next steps

* [Styling](/sdk/styling) — import styles in components
* [Capability context](/sdk/capability-context) — understand ctx
* [Each surface](/sdk/surfaces/storage) — detailed examples per feature


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.