# Ticket 1.0 — OpenCode API Contract Tests

**Phase**: 1 — OpenCode Bridge (Core)
**Priority**: Critical (blocking)
**Depends on**: 0.1, 0.2, 0.3

## Description

Create a suite of contract tests that validate our assumptions about OpenCode's API against a real `opencode serve` instance. OpenCode is under active development — its SSE event shapes, REST endpoint signatures, and permission/question flows may change between versions. These tests are our early-warning system.

### Why This Ticket Exists

Every ticket in Phase 1 (1.1–1.6) is built on assumptions about OpenCode's API that were derived by reading its source code. If those assumptions are wrong — or become wrong after an OpenCode update — we need to know immediately, not when the translator silently drops events or the permission bridge sends malformed replies.

Contract tests run against a real OpenCode server (not mocks) and assert the **shape** of responses, not their content. They answer: "Does the API still look the way we think it does?"

### OpenCode Version Pinning

The project should pin to a specific OpenCode version (git commit or release tag) and test against it. Upgrades should be deliberate:

1. Pin the version in a `.opencode-version` file or in the test config
2. Contract tests run against the pinned version in CI
3. A scheduled CI job (weekly) runs contract tests against OpenCode's `main` branch
4. When the scheduled job breaks, we know OpenCode changed and can adapt before upgrading

## Acceptance Criteria

### AC1: SSE event shape validation

```gherkin
Given a running OpenCode server with an active session
When a message is sent and the agent responds
Then the SSE stream emits events matching our expected shapes:
  - server.connected:       { type: "server.connected", properties: {} }
  - session.status (busy):  { type: "session.status", properties: { status: { type: "busy" } } }
  - message.part.updated:   { type: "message.part.updated", properties: { sessionID, messageID, part: { id, type, ... } } }
  - message.part.delta:     { type: "message.part.delta", properties: { sessionID, messageID, partID, field, delta } }
  - message.updated:        { type: "message.updated", properties: { sessionID, message: { id, role, ... } } }
  - session.status (idle):  { type: "session.status", properties: { status: { type: "idle" } } }
And each event is valid JSON with { type: string, properties: object } structure
```

### AC2: REST endpoint shape validation

```gherkin
Given a running OpenCode server
When we call each REST endpoint
Then the responses match our expected shapes:

  GET /global/health        → { healthy: boolean, version: string }
  GET /session/             → Array of session objects with { id, title, ... }
  POST /session/            → Session object with { id, ... }
  GET /session/:id          → Session object with full details
  GET /session/:id/message  → Array of message objects with { id, role, parts, ... }
  GET /agent/               → Array of agent objects with { id, name, ... }
  GET /provider/            → Array of provider objects
  GET /model/               → Array of model objects with { id, name, provider, ... }
  GET /command/             → Array of command objects
  GET /doc                  → OpenAPI 3.1 specification document
```

### AC3: Permission flow shape validation

```gherkin
Given a running OpenCode server with a session
When a tool execution requires permission (e.g., bash command)
Then a permission.asked SSE event is emitted with shape:
  { type: "permission.asked", properties: { id, sessionID, permission, patterns[], metadata, always[] } }

When we reply via POST /permission/:id/reply with { reply: "once" }
Then the server accepts the reply (200 OK)
And a permission.replied SSE event is emitted

When we call GET /permission/
Then it returns an array of pending permission requests (empty after reply)
```

### AC4: Question flow shape validation

```gherkin
Given a question.asked SSE event is emitted
Then it has shape:
  { type: "question.asked", properties: { id, sessionID, questions[] } }
And each question has: { question, header, options[], multiple?, custom? }
And each option has: { label, description }

When we reply via POST /question/:id/reply with { answers: [["option1"]] }
Then the server accepts the reply (200 OK)
And a question.replied SSE event is emitted
```

### AC5: OpenAPI spec snapshot comparison

```gherkin
Given a snapshot of OpenCode's OpenAPI spec exists in test/fixtures/opencode-api-snapshot.json
When we fetch GET /doc from the running server
Then we compare the response to the snapshot
And we report any differences (new endpoints, removed endpoints, changed schemas)
And the test WARNS (not fails) on additions but FAILS on removals or type changes
```

### AC6: Tool part state machine validation

```gherkin
Given a tool execution is triggered (e.g., read a file)
When we observe the SSE events for that tool's part
Then we see the expected state transitions:
  1. message.part.updated with part.type "tool", state.status "pending"
  2. message.part.updated with state.status "running"
  3. message.part.updated with state.status "completed" (or "error")
And each state includes the expected fields (callID, toolName, input, output)
```

### AC7: Version pinning validation

```gherkin
Given a .opencode-version file exists with a pinned version/commit
When the contract tests start
Then they verify the running OpenCode server's version matches the pin
And they log a warning if the versions don't match
And they skip with a clear message if the server isn't running
```

## Testable Components

| Component | Test Type | What to Assert |
|-----------|-----------|----------------|
| SSE event shapes | Contract (Vitest) | Events match TypeScript interfaces from types.ts |
| REST response shapes | Contract (Vitest) | Responses match expected schemas |
| Permission round-trip | Contract (Vitest) | Ask → reply → resolved lifecycle works |
| Question round-trip | Contract (Vitest) | Ask → reply lifecycle works |
| OpenAPI snapshot diff | Contract (Vitest) | No breaking changes from snapshot |
| Tool state machine | Contract (Vitest) | pending → running → completed transitions observed |
| Version check | Contract (Vitest) | Server version matches pinned version |

## Test Files

```
test/contract/
├── sse-events.contract.ts        # AC1: SSE event shape validation
├── rest-endpoints.contract.ts    # AC2: REST endpoint shapes
├── permission-flow.contract.ts   # AC3: Permission round-trip
├── question-flow.contract.ts     # AC4: Question round-trip
├── openapi-snapshot.contract.ts  # AC5: OpenAPI spec diff
├── tool-state-machine.contract.ts # AC6: Tool part lifecycle
├── version-check.contract.ts     # AC7: Version pinning
└── helpers/
    ├── server-connection.ts      # Connect to running OpenCode server
    └── session-helpers.ts        # Create/clean up test sessions
```

## Files to Create

- `test/contract/` directory and test files listed above
- `.opencode-version` — pinned OpenCode version (git commit hash or release tag)
- `test/fixtures/opencode-api-snapshot.json` — snapshot of OpenAPI spec from `GET /doc`

## CI Configuration

```yaml
# In CI pipeline (ticket 0.6):
# 1. Regular CI: contract tests are OPTIONAL (skip if no OpenCode server available)
# 2. Scheduled weekly job: starts OpenCode server, runs contract tests against main
# 3. Pre-upgrade job: run contract tests against new OpenCode version before bumping pin
```

## Notes

- Contract tests require a **real** OpenCode server — they are NOT unit tests
- They should be tagged/filtered separately: `vitest --project contract` or similar
- Tests should be **idempotent**: create test sessions, run assertions, clean up
- Keep tests focused on **shape**, not content (e.g., assert `typeof version === "string"`, not `version === "0.1.0"`)
- The OpenAPI snapshot (AC5) should be committed to git and updated deliberately
- These tests replace "hope" with "proof" that our assumptions hold

## Reference

- OpenCode server: `../opencode/packages/opencode/src/server/server.ts`
- OpenCode API routes: `../opencode/packages/opencode/src/server/routes/`
- OpenCode event bus types: `../opencode/packages/opencode/src/bus/`
- OpenCode OpenAPI spec: `GET /doc`
- Pinned version should match the OpenCode commit used during planning
