Testing
FORGE Studio uses Vitest for unit and integration tests, and Playwright for end-to-end tests.
Test Layout
test/
├── unit/
│ ├── main/ # Main process unit tests
│ │ ├── jsonl-parser.test.ts
│ │ ├── workflow-loader.test.ts
│ │ ├── forge-cli.test.ts
│ │ ├── compute-backend.test.ts
│ │ ├── stage-cache.test.ts
│ │ ├── twix-reader.test.ts
│ │ ├── ismrmrd-reader.test.ts
│ │ ├── yarra-converter.test.ts
│ │ ├── file-watcher.test.ts
│ │ ├── interpreter-session.test.ts
│ │ └── backend-dispatcher.test.ts
│ └── renderer/
│ ├── slices/ # Redux slice tests
│ └── keyboard/
├── integration/
│ └── workflow/
│ └── workflow-roundtrip.test.ts
├── e2e/ # Playwright tests (requires built app)
├── fixtures/ # Sample data
│ ├── workflows/ # Workflow YAML fixtures
│ ├── jsonl/ # FORGE stderr JSONL samples
│ ├── yarra/ # Yarra .mode fixtures
│ └── ismrmrd/ # ISMRMRD fixture data
└── helpers/
├── factories.ts # Test data factories
├── ipc-mock.ts # window.forgeAPI mock
└── redux-helpers.tsRunning Tests
# All tests (unit + integration)
npm run test
# Unit tests only
npm run test:unit
# Single file
npx vitest run test/unit/main/jsonl-parser.test.ts
# Watch mode
npx vitest
# E2E tests (requires npm run build first)
npm run test:e2eVitest Configurations
There are two Vitest configs because the main and renderer processes have different environments:
| Config | Environment | Covers |
|---|---|---|
vitest.config.ts | Node | Main process: parsers, CLI, cache, dispatch |
vitest.config.renderer.ts | jsdom | Renderer: Redux slices, component logic |
Both are run by npm run test. To run only one:
npx vitest run --config vitest.config.ts
npx vitest run --config vitest.config.renderer.tsTest Priorities
The highest-value tests are correctness-critical paths where a wrong result is silent:
| Test file | Why critical |
|---|---|
jsonl-parser.test.ts | Wrong event parsing produces silent bad metrics |
forge-cli.test.ts | Wrong CLI flags produce silent wrong reconstructions |
workflow-loader.test.ts | Invalid configs crash pipelines |
stage-cache.test.ts | Wrong hashing produces stale results |
twix-reader.test.ts | Wrong header values mislead the user |
Writing Main Process Tests
Main process tests use real filesystem operations with temp directories. Avoid mocking filesystem calls where possible — use tmp directories instead:
import { mkdtemp, rm } from 'fs/promises';
import { join } from 'path';
import { tmpdir } from 'os';
let tmpDir: string;
beforeEach(async () => {
tmpDir = await mkdtemp(join(tmpdir(), 'forge-test-'));
});
afterEach(async () => {
await rm(tmpDir, { recursive: true });
});Mock spawn and fs only when testing the protocol layer (e.g. backend-dispatcher.test.ts).
Writing Renderer Tests
Redux slice tests use the real store setup:
import { configureStore } from '@reduxjs/toolkit';
import jobReducer from '../../../src/renderer/store/jobSlice';
const store = configureStore({ reducer: { job: jobReducer } });
store.dispatch(someAction(payload));
expect(store.getState().job.someField).toBe(expectedValue);For tests that need the full store (with all slices), import from test/helpers/redux-helpers.ts.
Test Factories
test/helpers/factories.ts provides factory functions for common test objects:
import { makeJobSummary, makeWorkflowDef, makeTWIXHeader } from '../../helpers/factories';
const job = makeJobSummary({ status: 'failed' });
const wf = makeWorkflowDef({ stages: [...] });IPC Mock
Renderer tests that interact with window.forgeAPI use test/helpers/ipc-mock.ts:
import { setupIpcMock } from '../../helpers/ipc-mock';
beforeEach(() => setupIpcMock());The mock provides vi.fn() stubs for all ForgeAPI methods and push event subscriptions.
Fixture Data
Real-world data fixtures live in test/fixtures/:
| Directory | Contents |
|---|---|
fixtures/jsonl/ | Captured FORGE stderr JSONL event streams |
fixtures/workflows/ | Valid and invalid workflow YAML files |
fixtures/yarra/ | Yarra .mode files |
Use fixtures for snapshot tests and round-trip validation:
import { readFileSync } from 'fs';
import { join } from 'path';
const jsonl = readFileSync(join(__dirname, '../../fixtures/jsonl/cs_sense_run.jsonl'), 'utf8');GPU Tests
Tests that require real GPU hardware must be tagged so CI can skip them:
it.skipIf(process.env.CI)('uses Metal backend on Apple Silicon', async () => {
...
});Type Checking
npm run typecheck # tsc --noEmitAlways run typecheck before committing. All code must pass with zero errors — any types are prohibited.
Linting
npm run lint # ESLint