SDKsTesting

Testing

Causet applications are tested at three levels:

  1. Compiler validation — syntax and cross-reference checks on every PR
  2. Integration tests — submit intents, assert projection state
  3. Regression tests — replay historical events against a new IR version

There is no unit test framework for the DSL — the declarative nature of the language makes integration testing the primary quality gate.


Level 1: Compiler Validation (CI)

Run the compiler on every pull request. A non-zero exit code means the DSL has errors and should block merge.

# In CI
causet build compile --runtime ./my-app --out ./build/out

This catches:

  • Undefined field references
  • Invalid operation names
  • Non-deterministic expressions
  • Event type mismatches
  • Missing required fields

GitHub Actions example:

name: Compile Causet DSL
 
on: [push, pull_request]
 
jobs:
  compile:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-java@v4
        with:
          java-version: '21'
      - name: Compile DSL
        run: |
          causet build compile --runtime ./my-app --out ./build/out
      - name: Upload IR artifacts
        uses: actions/upload-artifact@v4
        with:
          name: causet-ir
          path: ./build/out/

Level 2: Integration Tests

Integration tests run against a live local stack and are the primary way to verify that your application behaves correctly end-to-end.

Setup

# Start the local stack
docker compose up -d
 
# Wait for services to be healthy
causet doctor --env local  # or check health endpoints manually
 
# Deploy your app to a test fork
# Via control plane UI or API

Test Structure

tests/
  integration/
    follow-artist.test.ts
    announce-show.test.ts
    ticket-purchase-saga.test.ts

Example Test (Node.js/Vitest + @causet/sdk-node)

import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { createCausetClient } from '@causet/sdk-node';
 
const PLATFORM = 'test-platform';
const APP = 'concert-app';
const FORK = `test-${Date.now()}`;
 
let client: ReturnType<typeof createCausetClient>;
 
beforeAll(async () => {
  client = createCausetClient({
    apiUrl: process.env.CAUSET_API_URL ?? 'http://localhost:8085',
    platformSlug: PLATFORM,
    appSlug: APP,
    forkId: FORK,
    apiKey: process.env.CAUSET_API_KEY,
  });
  await client.init();
});
 
afterAll(() => client.destroy());
 
describe('FOLLOW_ARTIST', () => {
  it('creates a user_following row', async () => {
    const result = await client.submitIntent('user_stream', 'user-1', 'FOLLOW_ARTIST', {
      user_id: 'user-1',
      artist_id: 'artist-1',
    });
    expect(result.accepted).toBe(true);
 
    // Wait for projection materialization
    await new Promise((r) => setTimeout(r, 500));
 
    const { items } = await client.runQuery('user_following_for_user', {
      user_id: 'user-1',
    });
    expect(items).toHaveLength(1);
    expect(items[0].artist_id).toBe('artist-1');
  });
 
  it('rejects self-follow', async () => {
    const result = await client.submitIntent('user_stream', 'user-1', 'FOLLOW_ARTIST', {
      user_id: 'user-1',
      artist_id: 'user-1',
    });
    expect(result.accepted).toBe(false);
  });
});

Test Isolation

Option 1 — separate fork per test run (recommended):

Create a unique fork ID for each CI run. Each fork gets its own tenant schema, preventing test data from leaking between runs.

const FORK = `test-${process.env.CI_RUN_ID ?? Date.now()}`;

The fork schema is created on first deployment:

causet fork create "$TEST_FORK" --parent main
causet deploy --fork "$TEST_FORK" --runtime .

causet migrations apply (applying DDL independently of a deploy) is proposed and has no CLI equivalent today — DDL is applied automatically as part of causet deploy.

Option 2 — truncate tables between tests:

TRUNCATE TABLE test_platform_concert_app_main.user_following CASCADE;
TRUNCATE TABLE test_platform_concert_app_main.artist_show_directory CASCADE;
-- ...

This is faster than creating a new fork but risks cross-test contamination if tests run in parallel.


Level 3: Regression Testing (Replay)

When changing a projection’s derive: expressions or adding new fields, replay historical events against the new IR to verify the output matches expectations.

Approach:

  1. Capture a snapshot of production projection table (or a fixture dataset)
  2. Deploy the new IR to a test fork
  3. Replay events from causet.ledger-events.v1 into the test fork
  4. Compare output against the expected snapshot
# Replay to test fork (sandbox-only)
causet recovery replay --stream user_stream --fork regression-test --from-cursor 0

causet replay scoped to a single named projection (shown elsewhere in this section) is proposed; causet recovery replay above is the real, shipped, stream-scoped equivalent.

Then query the test fork and assert the results match the expected values.


Proposed Test Utilities

Proposed: The following test utilities are on the roadmap.

causet test setup

Creates a clean test fork and applies the current IR:

causet test setup \
  --platform test-platform \
  --app concert-app \
  --fork $TEST_FORK

causet test teardown

Drops the test fork schema:

causet test teardown \
  --platform test-platform \
  --app concert-app \
  --fork $TEST_FORK

Projection Fixtures

Define fixture events in the DSL for each projection. The compiler validates that the fixture events produce the expected projection output:

# projections/user_following.projections.causet
projections:
  user_following:
    ...
    fixtures:
      - event_type: ARTIST_FOLLOWED
        payload:
          user_id:   "user-test"
          artist_id: "artist-test"
        expect:
          - user_id:     "user-test"
            artist_id:   "artist-test"

A causet projections doctor command that validates fixtures before deployment is proposed but not yet implemented; causet build validate is the real, shipped compile-time check.


What Not to Test

  • DSL expressions in isolation — you cannot unit test a SpEL expression outside the runtime. Test the behavior through intent submission + query.
  • Projection SQL — the compiler generates the SQL from your derive: and fields: blocks. Trust the compiler; test the end-to-end behavior.
  • Event ordering — per-entity ordering is guaranteed by the runtime. You do not need to test this.