Skip to content

Commit d8cd3cf

Browse files
sionsmithclaude
andcommitted
test: add validation tests for all resources and contributing guide
- Add test suites for expenses, projects, clients, contacts, tasks, users, and reports resources (was only time-entries, invoices, outcomes) - Add client instantiation tests verifying all resources are wired up - Add public API export validation tests (Keito, errors, OutcomeTypes, VERSION) - Add agentSummary blended reporting tests (time + outcomes + empty + failures) - Add CONTRIBUTING.md with development setup, conventional commits guide, testing conventions, PR process, and release process docs - Test count: 33 → 58 across 14 test files Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent c4ffdc9 commit d8cd3cf

10 files changed

Lines changed: 921 additions & 0 deletions

CONTRIBUTING.md

Lines changed: 270 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,270 @@
1+
# Contributing to @keito-ai/sdk
2+
3+
Thanks for your interest in contributing! This guide covers everything you need to get started.
4+
5+
## Table of Contents
6+
7+
- [Before You Start](#before-you-start)
8+
- [Development Setup](#development-setup)
9+
- [Project Structure](#project-structure)
10+
- [Code Style](#code-style)
11+
- [Conventional Commits](#conventional-commits)
12+
- [Testing](#testing)
13+
- [Pull Request Process](#pull-request-process)
14+
- [Adding a New Resource](#adding-a-new-resource)
15+
- [Regenerating Types](#regenerating-types)
16+
- [Release Process](#release-process)
17+
18+
## Before You Start
19+
20+
- Check [open issues](https://github.com/osodevops/keito-node/issues) before starting work — someone may already be on it.
21+
- For large changes, open an issue first to discuss the approach.
22+
- All contributions are released under the [MIT License](LICENSE).
23+
24+
## Development Setup
25+
26+
**Prerequisites:** Node.js 18+, npm
27+
28+
```bash
29+
# Clone the repo
30+
git clone https://github.com/osodevops/keito-node.git
31+
cd keito-node
32+
33+
# Install dependencies
34+
npm install
35+
36+
# Generate types from OpenAPI spec
37+
npm run generate
38+
39+
# Run the full validation pipeline
40+
npm run typecheck
41+
npm test
42+
npm run build
43+
```
44+
45+
### Development Scripts
46+
47+
| Script | Description |
48+
|--------|-------------|
49+
| `npm run generate` | Regenerate types from the OpenAPI spec |
50+
| `npm run typecheck` | Run TypeScript type checking |
51+
| `npm test` | Run all tests with Vitest |
52+
| `npm run test:watch` | Run tests in watch mode |
53+
| `npm run build` | Build ESM + CJS output to `dist/` |
54+
55+
## Project Structure
56+
57+
```
58+
src/
59+
index.ts # Public exports — everything users import
60+
client.ts # Keito class — wires resources to HTTP client
61+
version.ts # SDK version constant
62+
core/
63+
http.ts # Fetch wrapper, retries, auth, timeout
64+
errors.ts # Typed error hierarchy
65+
pagination.ts # Auto-pagination iterator
66+
generated/
67+
openapi.d.ts # AUTO-GENERATED — do not edit by hand
68+
models.ts # Re-export aliases for generated schema types
69+
resources/
70+
time-entries.ts # One file per API resource
71+
expenses.ts
72+
...
73+
outcome-types.ts # OutcomeTypes constants (F8)
74+
tests/
75+
client.test.ts # Client instantiation tests
76+
index.test.ts # Public API export validation
77+
core/
78+
http.test.ts # HTTP client tests (retries, errors, timeouts)
79+
pagination.test.ts # Pagination iterator tests
80+
resources/
81+
*.test.ts # One test file per resource
82+
```
83+
84+
### What's auto-generated vs hand-written
85+
86+
| Layer | Source | Changes when... |
87+
|-------|--------|----------------|
88+
| `src/generated/openapi.d.ts` | Auto-generated by `openapi-typescript` | Any spec field/schema changes |
89+
| `src/generated/models.ts` | Hand-written re-exports | New schemas added to spec |
90+
| `src/core/*` | Hand-written | Rarely (stable infrastructure) |
91+
| `src/resources/*` | Hand-written, referencing generated types | New endpoints added to spec |
92+
93+
**Never edit `src/generated/openapi.d.ts` by hand.** Run `npm run generate` instead.
94+
95+
## Code Style
96+
97+
- **TypeScript strict mode** — no `any`, no implicit returns, no unused variables.
98+
- **Snake_case for API fields** — matches the API. No camelCase transformation.
99+
- **`const` over `let`** — use `let` only when reassignment is necessary.
100+
- **Explicit return types** on public methods.
101+
- **No runtime dependencies** — the SDK uses only native `fetch`. Keep it that way.
102+
- **Arrays as `T[]`** not `Array<T>`.
103+
- **Interfaces for public params** — use `interface` for method parameter types (e.g. `ListTimeEntriesParams`), `type` for aliases.
104+
105+
## Conventional Commits
106+
107+
All commit messages **must** follow the [Conventional Commits](https://www.conventionalcommits.org/) format. This drives automatic versioning and changelog generation via release-please.
108+
109+
```
110+
<type>[optional scope]: <description>
111+
112+
[optional body]
113+
114+
[optional footer(s)]
115+
```
116+
117+
### Commit Types
118+
119+
| Type | Description | Version bump |
120+
|------|-------------|-------------|
121+
| `feat` | New feature | Minor (0.x: patch) |
122+
| `fix` | Bug fix | Patch |
123+
| `docs` | Documentation only | None |
124+
| `test` | Adding or updating tests | None |
125+
| `chore` | Maintenance (CI, deps, config) | None |
126+
| `refactor` | Code change that neither fixes a bug nor adds a feature | None |
127+
| `perf` | Performance improvement | Patch |
128+
129+
### Examples
130+
131+
```
132+
feat: add client.contacts.update() method
133+
fix: handle empty metadata in pagination response
134+
test: add validation tests for reports resource
135+
docs: update README with outcome billing examples
136+
chore: bump openapi-typescript to v8
137+
feat!: rename OutcomeLogParams.source to OutcomeLogParams.entry_source
138+
139+
BREAKING CHANGE: OutcomeLogParams.source has been renamed to entry_source
140+
```
141+
142+
A `BREAKING CHANGE` footer or `!` after the type triggers a major version bump (or minor while on 0.x).
143+
144+
## Testing
145+
146+
Every resource and core module must have tests. We use [Vitest](https://vitest.dev/).
147+
148+
### Test conventions
149+
150+
- **Mock `globalThis.fetch`** — tests never make real HTTP calls.
151+
- **One test file per source file**`src/resources/expenses.ts` -> `tests/resources/expenses.test.ts`.
152+
- **Test what the SDK sends** — verify the correct HTTP method, URL, query params, and request body.
153+
- **Test return values** — verify the response is correctly parsed and returned.
154+
- **Use `afterEach(() => vi.restoreAllMocks())`** — always clean up mocks.
155+
156+
### Running tests
157+
158+
```bash
159+
# Run all tests
160+
npm test
161+
162+
# Run in watch mode during development
163+
npm run test:watch
164+
165+
# Run a specific test file
166+
npx vitest run tests/resources/expenses.test.ts
167+
```
168+
169+
### Validation checklist
170+
171+
Before submitting a PR, ensure all of these pass:
172+
173+
```bash
174+
npm run generate # types regenerate without errors
175+
npm run typecheck # no TypeScript errors
176+
npm test # all tests pass
177+
npm run build # produces dist/ without errors
178+
```
179+
180+
## Pull Request Process
181+
182+
1. **Branch from `main`**: name your branch `feat/description`, `fix/description`, or `test/description`.
183+
2. **Write tests**: every new feature or bug fix needs tests.
184+
3. **Run the full pipeline**: `npm run generate && npm run typecheck && npm test && npm run build`.
185+
4. **Commit with conventional commits**: your PR title should also follow the format.
186+
5. **Keep PRs focused**: one logical change per PR. Split large changes into smaller PRs.
187+
6. **Link related issues**: use `closes #123` in the PR body.
188+
189+
### PR checklist
190+
191+
- [ ] Tests pass (`npm test`)
192+
- [ ] Type checking passes (`npm run typecheck`)
193+
- [ ] Build succeeds (`npm run build`)
194+
- [ ] Commit messages follow conventional commits
195+
- [ ] New public API surface is exported from `src/index.ts`
196+
- [ ] New resource has a corresponding test file
197+
198+
## Adding a New Resource
199+
200+
When a new endpoint is added to the OpenAPI spec:
201+
202+
1. **Regenerate types**: `npm run generate`
203+
2. **Add re-exports** to `src/generated/models.ts` for any new schemas.
204+
3. **Create the resource file** at `src/resources/<resource-name>.ts`:
205+
206+
```typescript
207+
import type { HttpClient } from '../core/http.js';
208+
import { paginate, PaginatedResponse } from '../core/pagination.js';
209+
import type { NewModel } from '../generated/models.js';
210+
211+
export interface ListNewModelsParams {
212+
page?: number;
213+
per_page?: number;
214+
// ... filters from the spec
215+
}
216+
217+
export class NewModels {
218+
constructor(private readonly http: HttpClient) {}
219+
220+
async list(params: ListNewModelsParams = {}): Promise<PaginatedResponse<NewModel>> {
221+
return paginate<NewModel>(this.http, '/api/v2/new_models', 'new_models', params);
222+
}
223+
}
224+
```
225+
226+
4. **Wire it up** in `src/client.ts` — add as a `readonly` property.
227+
5. **Export types** from `src/index.ts`.
228+
6. **Write tests** in `tests/resources/<resource-name>.test.ts`.
229+
7. **Run the full pipeline** to verify.
230+
231+
## Regenerating Types
232+
233+
The SDK types are auto-generated from the [Keito OpenAPI v2 spec](openapi/openapi-v2.yaml).
234+
235+
### Locally
236+
237+
```bash
238+
# Update the spec file in openapi/openapi-v2.yaml, then:
239+
npm run generate
240+
npm run typecheck
241+
```
242+
243+
### Via CI
244+
245+
The `generate.yml` workflow can be triggered manually or via `repository_dispatch` from the keito repo. It fetches the latest spec, regenerates types, and opens a PR if anything changed.
246+
247+
## Release Process
248+
249+
Releases are fully automated via [release-please](https://github.com/googleapis/release-please).
250+
251+
### How it works
252+
253+
1. Merge PRs with conventional commit messages to `main`.
254+
2. Release-please opens a "Release PR" that bumps `package.json`, updates `CHANGELOG.md`, and updates `.release-please-manifest.json`.
255+
3. When the Release PR is merged, release-please creates a git tag and GitHub Release.
256+
4. The publish job in `release-please.yml` builds and publishes to npm.
257+
258+
**You don't need to manually bump versions, create tags, or publish.** Just write good commit messages and merge.
259+
260+
### Manual release (emergency)
261+
262+
If you need to release outside the normal flow:
263+
264+
```bash
265+
# Update version in package.json and src/version.ts
266+
npm version patch # or minor, major
267+
git push --follow-tags
268+
```
269+
270+
The `release-please.yml` workflow will detect the tag and publish.

tests/client.test.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { describe, it, expect } from 'vitest';
2+
import { Keito } from '../src/client.js';
3+
import { TimeEntries } from '../src/resources/time-entries.js';
4+
import { Expenses } from '../src/resources/expenses.js';
5+
import { Projects } from '../src/resources/projects.js';
6+
import { Clients } from '../src/resources/clients.js';
7+
import { Contacts } from '../src/resources/contacts.js';
8+
import { Tasks } from '../src/resources/tasks.js';
9+
import { Users } from '../src/resources/users.js';
10+
import { Invoices, InvoiceMessages } from '../src/resources/invoices.js';
11+
import { Reports } from '../src/resources/reports.js';
12+
import { Outcomes } from '../src/resources/outcomes.js';
13+
14+
describe('Keito client', () => {
15+
it('exposes all resource instances', () => {
16+
const client = new Keito({ apiKey: 'kto_test', accountId: 'acc_test' });
17+
18+
expect(client.timeEntries).toBeInstanceOf(TimeEntries);
19+
expect(client.expenses).toBeInstanceOf(Expenses);
20+
expect(client.projects).toBeInstanceOf(Projects);
21+
expect(client.clients).toBeInstanceOf(Clients);
22+
expect(client.contacts).toBeInstanceOf(Contacts);
23+
expect(client.tasks).toBeInstanceOf(Tasks);
24+
expect(client.users).toBeInstanceOf(Users);
25+
expect(client.invoices).toBeInstanceOf(Invoices);
26+
expect(client.invoices.messages).toBeInstanceOf(InvoiceMessages);
27+
expect(client.reports).toBeInstanceOf(Reports);
28+
expect(client.outcomes).toBeInstanceOf(Outcomes);
29+
});
30+
31+
it('uses default baseUrl when not provided', () => {
32+
const client = new Keito({ apiKey: 'kto_test', accountId: 'acc_test' });
33+
34+
// All resources should be initialized — verifies constructor doesn't throw
35+
expect(client.timeEntries).toBeDefined();
36+
expect(client.outcomes).toBeDefined();
37+
});
38+
});

tests/index.test.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { describe, it, expect } from 'vitest';
2+
import * as sdk from '../src/index.js';
3+
4+
describe('Public API exports', () => {
5+
it('exports the Keito client class', () => {
6+
expect(sdk.Keito).toBeDefined();
7+
expect(typeof sdk.Keito).toBe('function');
8+
});
9+
10+
it('exports error classes', () => {
11+
expect(sdk.KeitoError).toBeDefined();
12+
expect(sdk.KeitoApiError).toBeDefined();
13+
expect(sdk.KeitoAuthError).toBeDefined();
14+
expect(sdk.KeitoForbiddenError).toBeDefined();
15+
expect(sdk.KeitoNotFoundError).toBeDefined();
16+
expect(sdk.KeitoConflictError).toBeDefined();
17+
expect(sdk.KeitoRateLimitError).toBeDefined();
18+
expect(sdk.KeitoTimeoutError).toBeDefined();
19+
expect(sdk.KeitoConnectionError).toBeDefined();
20+
});
21+
22+
it('exports PaginatedResponse', () => {
23+
expect(sdk.PaginatedResponse).toBeDefined();
24+
expect(typeof sdk.PaginatedResponse).toBe('function');
25+
});
26+
27+
it('exports OutcomeTypes constants', () => {
28+
expect(sdk.OutcomeTypes).toBeDefined();
29+
expect(sdk.OutcomeTypes.TICKET_RESOLVED).toBe('ticket_resolved');
30+
expect(sdk.OutcomeTypes.LEAD_QUALIFIED).toBe('lead_qualified');
31+
expect(sdk.OutcomeTypes.MEETING_BOOKED).toBe('meeting_booked');
32+
expect(sdk.OutcomeTypes.DOCUMENT_GENERATED).toBe('document_generated');
33+
expect(sdk.OutcomeTypes.CODE_REVIEW_COMPLETED).toBe('code_review_completed');
34+
expect(sdk.OutcomeTypes.DATA_PROCESSED).toBe('data_processed');
35+
expect(sdk.OutcomeTypes.EMAIL_SENT).toBe('email_sent');
36+
expect(sdk.OutcomeTypes.INVOICE_CREATED).toBe('invoice_created');
37+
expect(sdk.OutcomeTypes.WORKFLOW_COMPLETED).toBe('workflow_completed');
38+
expect(sdk.OutcomeTypes.CUSTOM).toBe('custom');
39+
});
40+
41+
it('exports VERSION', () => {
42+
expect(sdk.VERSION).toBeDefined();
43+
expect(typeof sdk.VERSION).toBe('string');
44+
expect(sdk.VERSION).toMatch(/^\d+\.\d+\.\d+/);
45+
});
46+
});

0 commit comments

Comments
 (0)