# Automated Test Triggers API

Automated test triggers allow izri to run E2E tests automatically in response to GitHub push and pull request events. The system integrates with the existing [webhook infrastructure](./webhooks.md) and reports results back to GitHub via Commit Status Checks.

---

## Overview

```
GitHub Push / PR Event
        │
        ▼
  Webhook Handler  (apps/api — Hono route)
        │
        ▼
  testTriggers.dispatchPush / dispatchPR  (tRPC)
        │
        ├─ Evaluate trigger conditions (branch, file patterns)
        ├─ Deduplicate (same commit SHA + event type)
        ├─ Debounce (optional, configurable window)
        │
        ▼
  TestTriggerService.runTrigger
        │
        ├─ Create test_runs record (status=PENDING)
        └─ Post GitHub commit status: pending
                │
                ▼
        Test runner executes tests
                │
        testTriggers.reportResult
                │
        ├─ Update test_runs record (PASSED / FAILED / ERROR)
        └─ Post GitHub commit status: success / failure / error
```

---

## tRPC Procedures

All procedures require authentication (protected).

### `testTriggers.dispatchPush`

Dispatch a test trigger from a GitHub `push` payload.

**Input:**

| Field | Type | Required | Description |
|---|---|---|---|
| `projectId` | `string (ULID)` | ✅ | Target project |
| `payload` | `GitHubPushPayload` | ✅ | Raw push webhook payload |
| `configOverride` | `TriggerConfig` | ❌ | Override stored config for this call |

**Output:**

```ts
// Triggered
{ triggered: true; testRunId: string; reason: string }

// Not triggered
{ triggered: false; reason: string }
```

**Example:**

```ts
const result = await trpc.testTriggers.dispatchPush.mutate({
  projectId: '01HX...',
  payload: githubPushPayload,
  configOverride: {
    branches: ['main', 'develop'],
    filePatterns: ['src/**/*.ts', 'packages/**'],
    debounceMs: 5000,
    prEvents: true,
  },
})
```

---

### `testTriggers.dispatchPR`

Dispatch a test trigger from a GitHub `pull_request` payload.

Only fires on actions: **`opened`**, **`synchronize`**, **`reopened`**.

**Input:**

| Field | Type | Required | Description |
|---|---|---|---|
| `projectId` | `string (ULID)` | ✅ | Target project |
| `payload` | `GitHubPRPayload` | ✅ | Raw pull_request webhook payload |
| `changedFiles` | `string[]` | ❌ | Files changed in the PR (from GitHub Files API) |
| `configOverride` | `TriggerConfig` | ❌ | Override stored config |

**Output:** Same shape as `dispatchPush`.

**Example:**

```ts
const result = await trpc.testTriggers.dispatchPR.mutate({
  projectId: '01HX...',
  payload: githubPRPayload,
  changedFiles: ['src/api/handler.ts', 'packages/trpc/src/router.ts'],
})
```

---

### `testTriggers.reportResult`

Update a test run's result and post the outcome to GitHub's Commit Status API.

**Input:**

| Field | Type | Required | Description |
|---|---|---|---|
| `testRunId` | `string` | ✅ | ID of the test run to update |
| `status` | `'PASSED' \| 'FAILED' \| 'ERROR'` | ✅ | Final outcome |
| `passedTests` | `number` | ❌ | Number of passing tests |
| `failedTests` | `number` | ❌ | Number of failing tests |
| `totalTests` | `number` | ❌ | Total test count |
| `error` | `string` | ❌ | Error message (for ERROR status) |
| `repoFullName` | `string` | ✅ | e.g. `"org/repo"` |
| `commitSha` | `string` | ✅ | Commit SHA to update status on |
| `githubToken` | `string` | ❌ | GitHub token with `repo:status` scope |
| `targetUrl` | `string` | ❌ | URL for the "Details" link in GitHub UI |

**Output:** `{ ok: true }`

---

### `testTriggers.getTestRun`

Retrieve a single test run by ID.

**Input:** `{ testRunId: string }`

**Output:** Full `TestRun` record or `null`.

---

### `testTriggers.listTestRuns`

List test runs for a project with optional filtering.

**Input:**

| Field | Type | Default | Description |
|---|---|---|---|
| `projectId` | `string (ULID)` | — | Required |
| `limit` | `number` | `20` | Max 100 |
| `status` | `enum` | — | `PENDING \| RUNNING \| PASSED \| FAILED \| ERROR` |
| `type` | `enum` | — | `push \| pr \| manual` |

**Output:** `{ runs: TestRun[] }`

---

## Trigger Configuration

Trigger conditions can be configured per project via the **webhooks config** (stored in `webhook_configs`) or overridden per-call with `configOverride`.

### `TriggerConfig` shape

```ts
interface TriggerConfig {
  /** Branches to watch on push events. Empty = all branches. */
  branches: string[]

  /**
   * Glob patterns for changed files. Empty = any change triggers.
   *
   * Evaluated using minimatch with `dot: true`.
   * Supports: `*`, `**`, `?`, `{a,b}` brace expansion, `[abc]` character classes.
   */
  filePatterns: string[]

  /** Trigger on PR opened/synchronize/reopened events. Default: true */
  prEvents: boolean

  /** Debounce window in ms. 0 = immediate. Max: 30,000 (30s). Default: 0 */
  debounceMs: number

  /** GitHub token with repo:status scope for posting commit statuses. */
  githubToken?: string
}
```

### Branch patterns

Exact string match against the branch name (after stripping `refs/heads/`).

```
branches: ["main", "develop", "release/v2"]
```

### File patterns

Glob patterns evaluated using [minimatch](https://github.com/isaacs/minimatch). Full glob syntax is supported:

| Pattern | Description | Example match |
|---|---|---|
| `*` | Any chars within one path segment | `src/*.ts` → `src/index.ts` |
| `**` | Zero or more path segments | `src/**/*.ts` → `src/api/auth.ts` |
| `?` | Any single character | `src/?.ts` → `src/a.ts` |
| `{a,b}` | Brace expansion | `*.{ts,tsx}` → `Button.tsx` |
| `[abc]` | Character class | `src/[abc]*.ts` → `src/auth.ts` |

**Examples:**

```ts
filePatterns: ["**/*.test.ts"]           // any test file anywhere in the repo
filePatterns: ["src/**/*.e2e.ts"]        // E2E tests under src/
filePatterns: ["packages/**", "apps/**"] // monorepo source changes
filePatterns: ["*.{json,yaml}"]          // config files at repo root
filePatterns: ["src/**/*.{ts,tsx}"]      // all TypeScript source files
filePatterns: [".github/**"]             // CI/workflow changes
```

---

## Debouncing & Deduplication

### Deduplication

Before creating a test run, the system checks for an existing run scoped to:
- Same `projectId`
- Same `commitSha`
- Same event type (`push` or `pull_request`)

within the last **1 hour**. Duplicate triggers are rejected with a descriptive reason.

### Debouncing

When `debounceMs > 0`, rapid triggers for the same `projectId + branch + eventType` are coalesced: each new trigger resets the timer, and only the final event (after the quiet period) is executed.

This prevents flooding the test runner when developers push multiple commits quickly.

```ts
configOverride: {
  debounceMs: 10_000, // wait 10s after last push before starting tests
}
```

---

## GitHub Status Checks

When a `githubToken` is configured, the system automatically posts commit statuses at two points:

| Lifecycle point | State | Description |
|---|---|---|
| Test run created | `pending` | "E2E tests are running…" |
| `reportResult` with `PASSED` | `success` | "X/Y tests passed" |
| `reportResult` with `FAILED` | `failure` | "X/Y tests failed" |
| `reportResult` with `ERROR` | `error` | Error message (truncated to 140 chars) |

The default status context is `izri/e2e`. This can be customised per integration.

### Required GitHub token scopes

- `repo:status` — post commit statuses on public and private repos

---

## Integration with Webhook System

The test trigger system extends the [webhook pipeline (issue #22)](./webhooks.md):

1. GitHub sends a push/PR event to the Hono webhook endpoint (`apps/api`)
2. The endpoint validates the HMAC-SHA256 signature and stores a `webhook_events` record
3. The webhook service calls `testTriggers.dispatchPush` or `testTriggers.dispatchPR`
4. Test trigger service evaluates conditions, creates a `test_runs` record, and posts a `pending` GitHub status
5. An external test runner (CI agent / queue worker) picks up the pending run, executes tests, and calls `testTriggers.reportResult`

---

## Data Model

Test runs are stored in the `test_runs` table:

```sql
id           VARCHAR(191) PRIMARY KEY  -- ULID
project_id   VARCHAR(191) NOT NULL
type         VARCHAR(32) NOT NULL      -- 'push' | 'pr' | 'manual'
status       VARCHAR(32) NOT NULL      -- 'PENDING' | 'RUNNING' | 'PASSED' | 'FAILED' | 'ERROR'
passed_tests INTEGER DEFAULT 0
failed_tests INTEGER DEFAULT 0
total_tests  INTEGER DEFAULT 0
skipped_tests INTEGER DEFAULT 0
coverage     REAL
duration     INTEGER                   -- ms
error        TEXT
logs         TEXT
results      JSONB                     -- includes commitSha for dedup
started_at   TIMESTAMP
completed_at TIMESTAMP
created_at   TIMESTAMP DEFAULT now()
user_id      VARCHAR(191) NOT NULL     -- triggering actor
```

---

## Error Handling

- **Signature invalid**: rejected at webhook layer before reaching trigger service
- **Unknown project**: `dispatchPush` / `dispatchPR` returns `triggered: false` with reason
- **Duplicate trigger**: returns `triggered: false` — not an error
- **GitHub Status API failure**: logged but does not fail the test run creation (best-effort)
- **Debounced trigger**: returns `triggered: false` with debounce reason — the real trigger fires asynchronously after the window

---

## Related

- [Webhooks API](./webhooks.md) — underlying event pipeline
- [`packages/trpc/src/routers/testTriggers.ts`](../../packages/trpc/src/routers/testTriggers.ts) — router implementation
- [`packages/trpc/src/services/testTriggerService.ts`](../../packages/trpc/src/services/testTriggerService.ts) — orchestration service
- [`packages/trpc/src/utils/testTriggers.ts`](../../packages/trpc/src/utils/testTriggers.ts) — pure utility functions
- [`packages/trpc/src/types/testTriggers.ts`](../../packages/trpc/src/types/testTriggers.ts) — type definitions
