> For the complete documentation index, see [llms.txt](https://gotts.gitbook.io/docs/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://gotts.gitbook.io/docs/monorepo-infrastructure/monorepo/04-build-test.md).

# Build and Test

> **Part of**: [Monorepo Infrastructure PRD](/docs/monorepo-infrastructure/monorepo.md) | **Last Updated**: 2026-02-19

***

## Build: tsdown

[tsdown](https://github.com/nicolo-ribaudo/tsdown) is the build tool for published packages. It is the active successor to tsup, powered by Rolldown (the Rust-based bundler behind Vite's future architecture).

> **Migration note**: tsup is deprecated as of late 2025. tsdown provides an identical DX with the `defineConfig` API while offering faster builds and active maintenance. The config format is nearly identical — most tsup configs work with tsdown after renaming the config file.

### tsdown Config

Each published package has a `tsdown.config.ts`:

```typescript
import { defineConfig } from "tsdown";

export default defineConfig({
  entry: ["src/index.ts"],
  format: ["esm", "cjs"],
  dts: true,
  clean: true,
  sourcemap: true,
  outDir: "dist",
});
```

**Options explained:**

| Option      | Value              | Purpose                                      |
| ----------- | ------------------ | -------------------------------------------- |
| `entry`     | `["src/index.ts"]` | Entry point(s) for the library               |
| `format`    | `["esm", "cjs"]`   | Dual-format output for maximum compatibility |
| `dts`       | `true`             | Generate `.d.ts` declaration files           |
| `clean`     | `true`             | Remove `dist/` before each build             |
| `sourcemap` | `true`             | Source maps for debugging                    |
| `outDir`    | `"dist"`           | Output directory                             |

### Multi-Entry Packages

For packages with multiple entry points (e.g., vault SDK + vault tools):

```typescript
import { defineConfig } from "tsdown";

export default defineConfig({
  entry: {
    index: "src/index.ts",
    tools: "src/tools/index.ts",
    client: "sdk/src/index.ts",
  },
  format: ["esm", "cjs"],
  dts: true,
  clean: true,
  sourcemap: true,
  outDir: "dist",
});
```

### Package Scripts

Each published package includes these scripts:

```jsonc
{
  "scripts": {
    "build": "tsdown",
    "dev": "tsdown --watch",
    "check-types": "tsc --noEmit",
    "clean": "rm -rf dist .turbo",
    "lint": "eslint .",
    "test": "vitest run",
  },
}
```

## Internal Packages Pattern

Internal packages (consumed only within the monorepo) skip the build step entirely. TypeScript source is imported directly.

### How It Works

The `package.json` `exports` field points to TypeScript source:

```jsonc
{
  "name": "@gotts.ai/ui",
  "version": "0.0.0",
  "private": true,
  "type": "module",
  "exports": {
    ".": {
      "types": "./src/index.ts",
      "default": "./src/index.ts",
    },
    "./*": {
      "types": "./src/*.ts",
      "default": "./src/*.ts",
    },
  },
}
```

**Why this works:**

1. TypeScript resolves types from the `types` condition
2. Bundlers (Next.js, Vite, tsdown) resolve source from the `default` condition and transpile it as part of their own build
3. No intermediate `dist/` directory, no stale build artifacts, no "did you rebuild?" issues

### When to Use Internal vs Published

| Signal                              | Pattern   | Build Required |
| ----------------------------------- | --------- | -------------- |
| Consumed only in this monorepo      | Internal  | No             |
| Published to npm                    | Published | Yes (tsdown)   |
| Shared configs (tsconfig, eslint)   | Internal  | No             |
| Shared UI components                | Internal  | No             |
| SDK consumed by external developers | Published | Yes (tsdown)   |

## Testing: Vitest 4.0+

[Vitest](https://vitest.dev) 4.0+ is the test runner, configured in **projects mode** at the root.

### Root `vitest.config.ts` (normative)

```typescript
import { defineConfig } from "vitest/config";

export default defineConfig({
  test: {
    projects: ["packages/*/vitest.config.ts", "apps/*/vitest.config.ts"],
  },
});
```

> **Note**: Vitest 4.0 replaced `vitest.workspace.ts` with the `projects` field inside `vitest.config.ts`. The workspace file is deprecated and should not be used.

### Per-Package `vitest.config.ts`

Each package with tests has its own config:

```typescript
import { defineConfig } from "vitest/config";

export default defineConfig({
  test: {
    name: "safe",
    environment: "node",
    include: ["test/**/*.test.ts"],
    globals: false,
    coverage: {
      provider: "v8",
      include: ["src/**/*.ts"],
      exclude: ["src/**/*.d.ts", "src/**/index.ts"],
      thresholds: {
        statements: 80,
        branches: 80,
        functions: 80,
        lines: 80,
      },
    },
  },
});
```

### Test File Conventions

| Convention | Rule                                                                  |
| ---------- | --------------------------------------------------------------------- |
| Location   | `test/` directory at package root                                     |
| Naming     | `*.test.ts` (unit), `*.integration.test.ts` (integration)             |
| Imports    | Explicit `import { describe, it, expect } from "vitest"` (no globals) |
| Mocking    | `vi.mock()` and `vi.fn()` — avoid jest-style `jest.mock()`            |
| Assertions | `expect()` API — no chai assertions                                   |

### Running Tests

```bash
# Run all tests across the monorepo
pnpm test

# Run tests for a specific package
pnpm turbo run test --filter=@gotts.ai/safe

# Run tests in watch mode (package-level)
cd packages/safe && pnpm vitest

# Run with coverage
cd packages/safe && pnpm vitest run --coverage

# Run integration tests (not cached by Turborepo)
pnpm test:integration
```

### Coverage Thresholds (normative)

| Metric     | Minimum | Target |
| ---------- | ------- | ------ |
| Statements | 80%     | 90%+   |
| Branches   | 80%     | 90%+   |
| Functions  | 80%     | 90%+   |
| Lines      | 80%     | 90%+   |

Coverage is enforced per-package, not monorepo-wide. Safety-critical packages (`safe`, `vault`, `agent-proxy`) should target 90%+.

### Solidity Testing

Solidity tests use Foundry's `forge test` and are **not** managed by Vitest or Turborepo. They are run via package-level scripts:

```jsonc
{
  "scripts": {
    "test:sol": "forge test",
    "test:sol:gas": "forge test --gas-report",
    "test:sol:slither": "slither contracts/src/ --config-file contracts/slither.config.json",
    "test:sol:aderyn": "aderyn contracts/ --config aderyn.toml",
  },
}
```

**Static analysis** (`test:sol:slither`, `test:sol:aderyn`) runs Slither and Aderyn respectively. Both require zero high-severity findings for merge. See the [Vault Testing PRD](/docs/gotts-vaults/vault/17-testing.md) §7 for configuration and severity gating.

**Formal verification** targets (Certora and Halmos) are defined in the [Vault Testing PRD](/docs/gotts-vaults/vault/17-testing.md) §8. Spec files live in `contracts/certora/` within each Solidity package.

Foundry configuration is defined in each package's `foundry.toml` and is covered in the [Vault PRD](/docs/gotts-vaults/vault.md).
