# Testing and CI

> Run xcodebuild test locally, CI workflow steps (design-token check, xcodegen, Debug test, Release build), test targets (STTFailureRouting, AXBudget, TranscriptRecord, DebugLogFormatter), and design-token enforcement via tools/check-design-tokens.sh.

- Repository: cartesia-ai/InkIt
- GitHub: https://github.com/cartesia-ai/InkIt
- Human docs: https://grok-wiki.com/public/docs/cartesia-ai-inkit-18975554254b
- Complete Markdown: https://grok-wiki.com/public/docs/cartesia-ai-inkit-18975554254b/llms-full.txt

## Source Files

- `.github/workflows/ci.yml`
- `AGENTS.md`
- `tools/check-design-tokens.sh`
- `InkItTests/STTFailureRoutingTests.swift`
- `InkItTests/AXBudgetTests.swift`
- `InkItTests/TranscriptRecordTests.swift`
- `InkItTests/DebugLogFormatterTests.swift`

---

---
title: Testing and CI
description: Run xcodebuild test locally, CI workflow steps (design-token check, xcodegen, Debug test, Release build), test targets (STTFailureRouting, AXBudget, TranscriptRecord, DebugLogFormatter), and design-token enforcement via tools/check-design-tokens.sh.
---

InkIt keeps a small, fast unit-test suite and a single GitHub Actions workflow. Together they verify design-system compliance, compile the app in Debug and Release, and lock regression contracts for STT failure routing, Accessibility traversal budgets, transcript persistence, and debug logging.

## Prerequisites

- macOS 14+ (matches `MACOSX_DEPLOYMENT_TARGET` in `project.yml`)
- Xcode 15+ with command-line tools
- [XcodeGen](https://github.com/yonaskolb/XcodeGen) (`brew install xcodegen`)

`InkIt.xcodeproj` is generated from `project.yml` — edit `project.yml`, not the `.pbxproj`.

## Run tests locally

<Steps>
<Step title="Regenerate the Xcode project">

After changing `project.yml` or adding test files:

```sh
xcodegen generate
```

</Step>

<Step title="Run the unit test suite">

```sh
xcodebuild test \
  -project InkIt.xcodeproj \
  -scheme InkIt \
  -configuration Debug \
  -destination 'platform=macOS'
```

Debug configuration uses ad-hoc signing with `get-task-allow=true` (`Config/Debug.xcconfig`) so XCTest can attach to the host app.

</Step>

<Step title="Check design tokens (optional but recommended)">

```sh
./tools/check-design-tokens.sh
```

Run this before pushing — CI fails the job on any unjustified literal.

</Step>

<Step title="Verify success">

Expect all tests in `InkItTests` to pass and the script to print:

```
✓ design tokens: no unjustified literals
```

</Step>
</Steps>

### Run a single test class

Filter by test target and class name:

```sh
xcodebuild test \
  -project InkIt.xcodeproj \
  -scheme InkIt \
  -configuration Debug \
  -destination 'platform=macOS' \
  -only-testing:InkItTests/STTFailureRoutingTests
```

## CI workflow

The **CI** workflow (`.github/workflows/ci.yml`) runs on every push and pull request to `main`, and can be triggered manually via `workflow_dispatch`.

| Setting | Value |
|---|---|
| Runner | `macos-15` |
| Timeout | 30 minutes |
| Concurrency | One job per ref; newer runs cancel in-progress jobs |
| Permissions | `contents: read` |

### Pipeline steps

```mermaid
flowchart LR
  A[Checkout] --> B[Tool versions]
  B --> C[Design tokens]
  C --> D[Install XcodeGen]
  D --> E[xcodegen generate]
  E --> F[Debug test]
  F --> G[Release build]
```

<AccordionGroup>
<Accordion title="1. Check out repository">

`actions/checkout@v4` clones the ref under test.

</Accordion>

<Accordion title="2. Show tool versions">

Prints `sw_vers` and `xcodebuild -version` for reproducibility in CI logs.

</Accordion>

<Accordion title="3. Check design tokens">

```sh
./tools/check-design-tokens.sh
```

Fails fast before any compile step if a view introduces a hardcoded design value.

</Accordion>

<Accordion title="4. Install build tools">

```sh
brew install xcodegen
```

</Accordion>

<Accordion title="5. Generate Xcode project">

```sh
xcodegen generate
```

Produces `InkIt.xcodeproj` from `project.yml`.

</Accordion>

<Accordion title="6. Run unit tests (Debug)">

```sh
xcodebuild test \
  -project InkIt.xcodeproj \
  -scheme InkIt \
  -configuration Debug \
  -destination 'platform=macOS' \
  -derivedDataPath build/ci-debug \
  -quiet
```

Runs the full `InkItTests` bundle against a Debug-built host app.

</Accordion>

<Accordion title="7. Build Release app">

```sh
xcodebuild build \
  -project InkIt.xcodeproj \
  -scheme InkIt \
  -configuration Release \
  -destination 'platform=macOS' \
  -derivedDataPath build/ci-release \
  -quiet
```

Verifies the Release configuration compiles. CI does not run tests in Release.

</Accordion>
</AccordionGroup>

## Test target: InkItTests

`InkItTests` is a macOS unit-test bundle defined in `project.yml`. It links against the main `InkIt` app target and uses the app as its test host:

| Setting | Value |
|---|---|
| Type | `bundle.unit-test` |
| Bundle ID | `ai.cartesia.InkItTests` |
| `TEST_HOST` | `$(BUILT_PRODUCTS_DIR)/InkIt.app/Contents/MacOS/InkIt` |
| Signing | Ad-hoc (`CODE_SIGN_IDENTITY = -`), signing disabled on the bundle |

All four test files live under `InkItTests/` and import `@testable import InkIt`.

### STTFailureRoutingTests

Locks the end-of-session contract in `CartesiaStreamingClient.reportFailureOrCollapse`. The "Couldn't transcribe" notch fires only for **named, actionable** failures when there is nothing useful to deliver.

| Test area | What it guards |
|---|---|
| Graceful goodbye | `.unknown` with transcript content delivers words via `onClosed`, never `onError` |
| Silent tap | `.unknown` with no content collapses silently (empty transcript, no error) |
| Named failures | `.offline`, `.serverError`, `.rateLimited`, `.outOfCredits`, `.invalidKey` always reach `onError` and do not also fire `onClosed` |
| Post-close 500 carve-out | `.serverError` after close with no content collapses silently; mid-hold 5xx still surfaces |
| Classification | Benign POSIX disconnects (`ENOTCONN`, `EPIPE`, `ECONNRESET`) classify as `.unknown`; real transport errors stay named |

These tests are the regression anchor for STT error UX documented on the STT troubleshooting page.

### AXBudgetTests

Locks `AX.run(budget:_:)` in `PasteService.swift` — the shared Accessibility traversal primitive. Every AX walk routes through this helper.

| Test | Property verified |
|---|---|
| `testReturnsClosureResult` | Closure return value propagates |
| `testDeadlineIsBudgetInTheFuture` | Deadline is approximately `budget` seconds ahead |
| `testClosureHonorsDeadlineAndReturnsPartialWork` | Long work stops at the deadline instead of running unbounded |
| `testRunsOffTheMainThread` | Work executes off the main thread |

This prevents regressions where a slow AX tree stalls the main run loop and freezes modifier keys or drops pastes.

### TranscriptRecordTests

Locks the SwiftData persistence contract between `TranscriptHistoryStore.Entry` and `TranscriptRecord`. Tests use an in-memory `ModelContainer` — no disk, no singleton.

| Test | What it guards |
|---|---|
| `testPolishedRecordRoundTripsAllFields` | Full entry with latency, original text, and polish outcome survives insert/fetch |
| `testFailedRecordRoundTripsFailureDetails` | `PolishFailure` composite attributes (reason, provider, retryAt) persist |
| `testMinimalRecordRoundTrips` | Legacy nil-valued fields round-trip |
| `testIDIsPreservedThroughMapping` | Entry ID is stable through `Entry → TranscriptRecord → Entry` |
| `testFetchSortsNewestFirst` | Timestamp sort descriptor returns newest-first order |

### DebugLogFormatterTests

Locks `DebugLog` helpers used when `debugLoggingEnabled` writes trace output to `~/Library/Logs/InkIt-debug.log`.

| Test | Behavior verified |
|---|---|
| `testBoundedBlockIncludesMetadataAndTruncates` | `boundedBlock` reports byte count, hash, `truncated=true`, and prefixes body |
| `testBoundedBlockMarksUntruncated` | Short text gets `truncated=false` |
| `testRedactsSecrets` | `redacted(_:secrets:)` replaces known secrets with `<redacted>` |
| `testPrettyJSONStringDoesNotInjectAPIKey` | `prettyJSONString` serializes the payload without injecting credentials |

## Design-token enforcement

InkIt enforces a single source of truth for typography, color, motion, and elevation: the `Font` and `Color` extensions (plus `Motion`, `Elevation`, `Radius`) at the top of `InkIt/InkItApp.swift`. Views reference named tokens (`Font.inkBody`, `Color.canvas`, `Motion.quick`, …) instead of inline literals.

`tools/check-design-tokens.sh` scans `InkIt/*.swift` and **exits 1** on any match that lacks justification.

### Patterns checked

| Pattern | Label | Fix |
|---|---|---|
| `system(size: N` | Hardcoded font size | Use a `Font.ink*` token |
| `Color(red:` / `Color(white:` | Raw color literal | Use a `Color` token or system semantic |
| `easeOut(duration:` | Hardcoded animation timing | Use a `Motion.*` token |
| `.black.opacity(` | Raw shadow/scrim ink | Use `Elevation.*` or `Color.scrim` |

### Exemptions

The script skips:

- Lines inside `static let` token definitions
- Lines containing `// ds-allow: <reason>` (the sanctioned one-off escape hatch)
- Comment-only lines

### Adding a justified one-off

For genuine exceptions — SF Symbol icon glyphs, the always-dark notch HUD, dual-appearance `AppearanceThumbnail` previews, bespoke reveal animations — append a trailing comment:

```swift
Image(systemName: "gearshape")
    .font(.system(size: 17, weight: .medium))  // ds-allow: icon
```

Recurring values belong in the token block, not behind `ds-allow`. Full rationale lives in `DESIGN_SYSTEM.md` and the contributor summary in `AGENTS.md`.

### Example failure output

```
✗ hardcoded font size — use a Font.ink* token:
    InkIt/SomeView.swift:42:        .font(.system(size: 15))

Found 1 hardcoded design value(s) outside the design system.
Use a Font.ink* / Color token, or justify a true one-off with  // ds-allow: <reason>
```

## What CI does not cover

CI validates compile-time correctness and the four unit-test contracts. It does **not**:

- Run UI or integration tests against live Cartesia or LLM APIs
- Exercise Accessibility grants, microphone capture, or global hotkey registration
- Produce a notarized Release DMG (see the release workflow in local tooling)

For runtime issues outside the test suite, use debug logging and the runtime troubleshooting guides.

## Related pages

<CardGroup cols={2}>
<Card title="Build from source" href="/build-from-source">
Generate the Xcode project, understand Debug vs Release signing, and install locally.
</Card>

<Card title="Contributing" href="/contributing">
Design-system token rules, commit-message conventions, and what CI enforces on every PR.
</Card>

<Card title="STT troubleshooting" href="/stt-troubleshooting">
Diagnose Cartesia transcription failures guarded by STTFailureRoutingTests.
</Card>

<Card title="Paste and focus reference" href="/paste-and-focus-reference">
AX budget walks, paste timing, and the production code behind AXBudgetTests.
</Card>
</CardGroup>
