Skip to content

Commit 2a00056

Browse files
Merge branch 'add-tests-u0bb'
2 parents a4c787c + 56c6a5d commit 2a00056

16 files changed

Lines changed: 3047 additions & 12 deletions

File tree

.github/workflows/ci.yml

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
name: CI
2+
3+
on:
4+
pull_request:
5+
branches:
6+
- main
7+
8+
jobs:
9+
check:
10+
name: Lint & Typecheck
11+
runs-on: ubuntu-latest
12+
steps:
13+
- name: Checkout
14+
uses: actions/checkout@v4
15+
16+
- name: Setup Bun
17+
uses: oven-sh/setup-bun@v2
18+
19+
- name: Setup mise
20+
uses: jdx/mise-action@v2
21+
22+
- name: Install dependencies
23+
run: bun install
24+
25+
- name: Run lint
26+
run: mise run lint
27+
28+
- name: Run typecheck
29+
run: mise run typecheck
30+
31+
test:
32+
name: Test
33+
runs-on: ubuntu-latest
34+
steps:
35+
- name: Checkout
36+
uses: actions/checkout@v4
37+
38+
- name: Setup Bun
39+
uses: oven-sh/setup-bun@v2
40+
41+
- name: Setup mise
42+
uses: jdx/mise-action@v2
43+
44+
- name: Setup uv
45+
uses: astral-sh/setup-uv@v4
46+
47+
- name: Install dependencies
48+
run: bun install
49+
50+
- name: Run tests
51+
run: mise run test

.github/workflows/publish.yml

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,52 @@ on:
66
- main
77

88
jobs:
9+
check:
10+
name: Lint & Typecheck
11+
runs-on: ubuntu-latest
12+
steps:
13+
- name: Checkout
14+
uses: actions/checkout@v4
15+
16+
- name: Setup Bun
17+
uses: oven-sh/setup-bun@v2
18+
19+
- name: Setup mise
20+
uses: jdx/mise-action@v2
21+
22+
- name: Install dependencies
23+
run: bun install
24+
25+
- name: Run lint
26+
run: mise run lint
27+
28+
- name: Run typecheck
29+
run: mise run typecheck
30+
31+
test:
32+
name: Test
33+
runs-on: ubuntu-latest
34+
steps:
35+
- name: Checkout
36+
uses: actions/checkout@v4
37+
38+
- name: Setup Bun
39+
uses: oven-sh/setup-bun@v2
40+
41+
- name: Setup mise
42+
uses: jdx/mise-action@v2
43+
44+
- name: Setup uv
45+
uses: astral-sh/setup-uv@v4
46+
47+
- name: Install dependencies
48+
run: bun install
49+
50+
- name: Run tests
51+
run: mise run test
52+
953
publish:
54+
needs: [check, test]
1055
runs-on: ubuntu-latest
1156
environment: npm-publish
1257
permissions:

mise.toml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,14 @@ fi
121121
NODE_ENV=production HOST="$HOST" bun server/index.ts
122122
"""
123123

124+
[tasks.test]
125+
description = "Run all tests"
126+
run = "bun test"
127+
128+
[tasks."test:watch"]
129+
description = "Run tests in watch mode"
130+
run = "bun test --watch"
131+
124132
[tasks.lint]
125133
description = "Run ESLint"
126134
run = "bunx eslint ."

server/__tests__/fixtures/app.ts

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import { createApp } from '../../app'
2+
3+
/**
4+
* Test client for making requests to the Hono app.
5+
*/
6+
export interface TestAppClient {
7+
/** The Hono app instance */
8+
app: ReturnType<typeof createApp>
9+
10+
/** Make a GET request */
11+
get: (path: string, headers?: Record<string, string>) => Promise<Response>
12+
13+
/** Make a POST request with JSON body */
14+
post: (path: string, body?: unknown, headers?: Record<string, string>) => Promise<Response>
15+
16+
/** Make a PATCH request with JSON body */
17+
patch: (path: string, body?: unknown, headers?: Record<string, string>) => Promise<Response>
18+
19+
/** Make a PUT request with JSON body */
20+
put: (path: string, body?: unknown, headers?: Record<string, string>) => Promise<Response>
21+
22+
/** Make a DELETE request */
23+
delete: (path: string, headers?: Record<string, string>) => Promise<Response>
24+
25+
/** Make a request with full control */
26+
request: (path: string, init?: RequestInit) => Promise<Response>
27+
}
28+
29+
/**
30+
* Creates a test client for the Hono app.
31+
* Uses Hono's native request method for testing without a real server.
32+
*/
33+
export function createTestApp(): TestAppClient {
34+
const app = createApp()
35+
36+
const request = async (path: string, init?: RequestInit): Promise<Response> => {
37+
const url = `http://localhost${path}`
38+
return app.request(url, init)
39+
}
40+
41+
return {
42+
app,
43+
request,
44+
45+
get: (path: string, headers?: Record<string, string>) => {
46+
return request(path, {
47+
method: 'GET',
48+
headers,
49+
})
50+
},
51+
52+
post: (path: string, body?: unknown, headers?: Record<string, string>) => {
53+
return request(path, {
54+
method: 'POST',
55+
headers: {
56+
'Content-Type': 'application/json',
57+
...headers,
58+
},
59+
body: body !== undefined ? JSON.stringify(body) : undefined,
60+
})
61+
},
62+
63+
patch: (path: string, body?: unknown, headers?: Record<string, string>) => {
64+
return request(path, {
65+
method: 'PATCH',
66+
headers: {
67+
'Content-Type': 'application/json',
68+
...headers,
69+
},
70+
body: body !== undefined ? JSON.stringify(body) : undefined,
71+
})
72+
},
73+
74+
put: (path: string, body?: unknown, headers?: Record<string, string>) => {
75+
return request(path, {
76+
method: 'PUT',
77+
headers: {
78+
'Content-Type': 'application/json',
79+
...headers,
80+
},
81+
body: body !== undefined ? JSON.stringify(body) : undefined,
82+
})
83+
},
84+
85+
delete: (path: string, headers?: Record<string, string>) => {
86+
return request(path, {
87+
method: 'DELETE',
88+
headers,
89+
})
90+
},
91+
}
92+
}

server/__tests__/fixtures/db.ts

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import { db, resetDatabase } from '../../db'
2+
import {
3+
tasks,
4+
terminals,
5+
terminalTabs,
6+
terminalViewState,
7+
repositories,
8+
systemMetrics,
9+
} from '../../db/schema'
10+
11+
/**
12+
* Clears all data from all tables.
13+
* Call this in afterEach() to ensure test isolation.
14+
*/
15+
export async function clearDatabase(): Promise<void> {
16+
// Delete in order to avoid foreign key issues (though SQLite doesn't enforce them by default)
17+
await db.delete(systemMetrics)
18+
await db.delete(terminals)
19+
await db.delete(terminalTabs)
20+
await db.delete(terminalViewState)
21+
await db.delete(tasks)
22+
await db.delete(repositories)
23+
}
24+
25+
/**
26+
* Pushes the schema to the database.
27+
* For tests, we use drizzle-kit push equivalent.
28+
* Since we're using VIBORA_DIR isolation, the database will be fresh.
29+
*/
30+
export async function pushSchema(): Promise<void> {
31+
// The database is lazily initialized when first accessed via the db proxy.
32+
// drizzle-kit push is typically run separately, but for tests we rely on
33+
// the schema being defined. In bundled mode migrations are run, but in tests
34+
// we need to create tables manually.
35+
36+
// Access db to trigger lazy initialization which creates the vibora directory
37+
// and database file. Tables are created by drizzle-kit push in dev mode.
38+
39+
// For tests, we'll run drizzle-kit push via CLI or ensure tables exist.
40+
// Since we want no mocks and real behavior, we use the actual drizzle-kit.
41+
}
42+
43+
/**
44+
* Helper to insert a test task.
45+
*/
46+
export async function insertTestTask(data: {
47+
id?: string
48+
title: string
49+
repoPath: string
50+
repoName?: string
51+
baseBranch?: string
52+
status?: string
53+
position?: number
54+
}): Promise<typeof tasks.$inferSelect> {
55+
const now = new Date().toISOString()
56+
const id = data.id ?? crypto.randomUUID()
57+
58+
const [task] = await db
59+
.insert(tasks)
60+
.values({
61+
id,
62+
title: data.title,
63+
status: data.status ?? 'IN_PROGRESS',
64+
position: data.position ?? 0,
65+
repoPath: data.repoPath,
66+
repoName: data.repoName ?? 'test-repo',
67+
baseBranch: data.baseBranch ?? 'main',
68+
createdAt: now,
69+
updatedAt: now,
70+
})
71+
.returning()
72+
73+
return task
74+
}
75+
76+
/**
77+
* Helper to insert a test repository.
78+
*/
79+
export async function insertTestRepository(data: {
80+
id?: string
81+
path: string
82+
displayName?: string
83+
}): Promise<typeof repositories.$inferSelect> {
84+
const now = new Date().toISOString()
85+
const id = data.id ?? crypto.randomUUID()
86+
87+
const [repo] = await db
88+
.insert(repositories)
89+
.values({
90+
id,
91+
path: data.path,
92+
displayName: data.displayName ?? 'Test Repo',
93+
createdAt: now,
94+
updatedAt: now,
95+
})
96+
.returning()
97+
98+
return repo
99+
}
100+
101+
// Re-export for convenience
102+
export { db, resetDatabase }

0 commit comments

Comments
 (0)