Skip to main content

Overview

The ClarkOS SDK includes a comprehensive Jest test framework with 179 tests across 7 test suites, providing full coverage of core functionality.

Test Suite

See the complete test suite on GitHub

Quick Start

# Run all tests
npm test

# Run tests in watch mode
npm run test:watch

# Generate coverage report
npm run test:coverage

Test Coverage

ModuleTestsDescription
core/tick25Routine calculation, health drift, tick execution, tick runner
core/config22Configuration validation, environment loading, Zod schemas
memory/deduplication343-tier dedup strategy, type thresholds, similarity
backend/memory22In-memory backend, state management, CRUD operations
plugins/loader26Plugin validation, dependency sorting, cycle detection
llm/embeddings32Cosine similarity, find similar, provider configurations
services/news21Service lifecycle, caching, deduplication
Total179All passing

Test Structure

ink/
├── jest.config.js           # Jest configuration
├── tests/
│   ├── setup.ts             # Test utilities and custom matchers
│   ├── core/
│   │   ├── tick.test.ts     # Tick system tests
│   │   └── config.test.ts   # Configuration tests
│   ├── memory/
│   │   └── deduplication.test.ts  # Deduplication tests
│   ├── backend/
│   │   └── memory.test.ts   # In-memory backend tests
│   ├── plugins/
│   │   └── loader.test.ts   # Plugin loader tests
│   ├── llm/
│   │   └── embeddings.test.ts  # Embedding client tests
│   └── services/
│       └── news.test.ts     # News service tests

Writing Tests

Basic Test Structure

import { jest, describe, it, expect, beforeEach } from '@jest/globals';
import { myFunction } from '../../src/module/file.js';

describe('myFunction', () => {
  beforeEach(() => {
    // Setup before each test
  });

  it('does something expected', () => {
    const result = myFunction('input');
    expect(result).toBe('expected');
  });

  it('handles edge cases', () => {
    expect(() => myFunction(null)).toThrow();
  });
});

Using MemoryBackend for Tests

The MemoryBackend is ideal for testing without a real Convex deployment:
import { MemoryBackend } from '../../src/backend/memory.js';

describe('MyFeature', () => {
  let backend: MemoryBackend;

  beforeEach(() => {
    backend = new MemoryBackend();
  });

  afterEach(() => {
    backend.reset(); // Clear all data between tests
  });

  it('stores and retrieves data', async () => {
    await backend.storeMemory({
      content: 'Test memory',
      type: 'semantic',
      scope: 'working',
      importance: 0.5,
      // ... other required fields
    });

    const memories = await backend.getMemories({ type: 'semantic' });
    expect(memories.length).toBe(1);
  });
});

Testing Plugins

import { executeTick } from '../../src/core/tick.js';
import { MemoryBackend } from '../../src/backend/memory.js';
import { createConfig } from '../../src/core/config.js';
import type { Plugin } from '../../src/plugins/types.js';

describe('Plugin Integration', () => {
  it('calls plugin onTick hook', async () => {
    const backend = new MemoryBackend();
    const config = createConfig({ verbose: false });

    const onTickMock = jest.fn();
    const plugin: Plugin = {
      name: 'test-plugin',
      version: '1.0.0',
      onTick: onTickMock,
    };

    await executeTick(backend, [plugin], config);

    expect(onTickMock).toHaveBeenCalledTimes(1);
    expect(onTickMock).toHaveBeenCalledWith(
      expect.objectContaining({
        state: expect.any(Object),
        memories: expect.any(Array),
      })
    );
  });
});

Mocking External APIs

For tests that call external APIs (LLM, embeddings), mock the fetch function:
import { jest, beforeEach, afterEach } from '@jest/globals';

describe('EmbeddingClient', () => {
  beforeEach(() => {
    global.fetch = jest.fn().mockResolvedValue({
      ok: true,
      json: () => Promise.resolve({
        embedding: { values: [0.1, 0.2, 0.3] }
      }),
    });
  });

  afterEach(() => {
    jest.restoreAllMocks();
  });

  it('calls the API', async () => {
    // Your test using the mocked fetch
    expect(global.fetch).toHaveBeenCalled();
  });
});

Test Configuration

The framework uses Jest with ESM support:
// jest.config.js
export default {
  preset: 'ts-jest/presets/default-esm',
  testEnvironment: 'node',
  extensionsToTreatAsEsm: ['.ts', '.tsx'],
  coverageThreshold: {
    global: {
      branches: 70,
      functions: 70,
      lines: 70,
      statements: 70,
    },
  },
};

Running Specific Tests

# Run a specific test file
npm test -- tests/core/tick.test.ts

# Run tests matching a pattern
npm test -- --testPathPattern="memory"

# Run a specific test by name
npm test -- -t "calculates routine"

Coverage Reports

After running npm run test:coverage:
  • coverage/lcov-report/index.html - HTML report (open in browser)
  • coverage/lcov.info - LCOV format for CI integration
  • Terminal output shows summary

What’s Tested

Tick System

  • Routine boundaries (6am, 12pm, 6pm, 12am)
  • Health drift calculations with mean reversion toward 75
  • Cryo mode behavior
  • Plugin hook execution
  • Error handling and recovery

Deduplication

  • All 5 memory type thresholds:
    • Episodic: 0.92
    • Semantic: 0.95
    • Emotional: 0.88
    • Procedural: 0.97
    • Reflection: 0.90
  • 3-tier strategy:
    1. Exact content match
    2. Jaccard similarity (word overlap > 0.85)
    3. Embedding similarity (type-specific)

Plugin System

  • Plugin validation rules
  • Dependency resolution (topological sort)
  • Circular dependency detection
  • Lifecycle hooks (init, cleanup, onTick)

Embeddings

  • Cosine similarity calculations
  • Provider configuration (Gemini, OpenAI, custom)
  • Batch embedding support
  • Error handling for API failures

Best Practices

Each test should be independent. Use beforeEach to reset state and afterEach for cleanup.
Focus on what the function does, not how it does it. This makes tests more resilient to refactoring.
Test names should describe the expected behavior: “returns error when agent is in cryo mode” not “test cryo”.
Mock external dependencies. Use MemoryBackend instead of real Convex connections.
Include tests for error conditions, empty inputs, and boundary values.

CI Integration

Example GitHub Actions workflow:
name: Tests
on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'

      - name: Install dependencies
        run: |
          cd ink
          npm ci

      - name: Run tests
        run: |
          cd ink
          npm test

      - name: Upload coverage
        uses: codecov/codecov-action@v3
        with:
          files: ./ink/coverage/lcov.info

Next Steps