Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion backend/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -340,7 +340,7 @@ app.route('/api/auth-info', createAuthInfoRoutes(auth, db))
app.route('/api/health', createHealthRoutes(db, openCodeSupervisor))

app.route('/api/mcp-oauth-proxy', createMcpOauthProxyRoutes(openCodeClient, requireAuth))
app.route('/api/internal', createInternalRoutes(db, scheduleService, notificationService, settingsService, openCodeClient, sessionPermissionModeService, repoWorkspaces, gitAuthService))
app.route('/api/internal', createInternalRoutes(db, scheduleService, notificationService, settingsService, openCodeClient, sessionPermissionModeService, repoWorkspaces, gitAuthService, sessionGoalService, multiRunService))
app.route('/api/opencode-proxy', createOpenCodeProxyRoutes(db, settingsService))

const protectedApi = new Hono()
Expand Down
8 changes: 8 additions & 0 deletions backend/src/routes/internal/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,13 @@ import { createInternalSessionRoutes } from './sessions'
import { createInternalAssistantRoutes } from './assistant'
import { createInternalGitCredentialsRoutes } from './git-credentials'
import { createInternalSandboxRoutes } from './sandbox'
import { createSessionGoalRoutes } from '../session-goals'
import { createMultiRunRoutes } from '../multi-runs'
import type { SessionPermissionModeService } from '../../services/session-permission-modes'
import type { RepoWorkspaceService } from '../../services/repo-workspace'
import type { GitAuthService } from '../../services/git-auth'
import type { SessionGoalService } from '../../services/session-goals'
import type { MultiRunService } from '../../services/multi-runs'

export function createInternalRoutes(
db: Database,
Expand All @@ -30,10 +34,14 @@ export function createInternalRoutes(
permissionModes: SessionPermissionModeService,
repoWorkspaces: RepoWorkspaceService,
gitAuthService: GitAuthService,
sessionGoals: SessionGoalService,
multiRuns: MultiRunService,
) {
const app = new Hono()
app.use('/*', createInternalTokenMiddleware(db))
app.route('/schedules', createScheduleRoutes(scheduleService))
app.route('/session-goals', createSessionGoalRoutes(sessionGoals))
app.route('/multi-runs', createMultiRunRoutes(multiRuns))
app.route('/notifications', createInternalNotificationRoutes(notificationService))
app.route('/settings', createInternalSettingsRoutes(settingsService))
app.route('/opencode-config', createOpenCodeConfigRoutes(settingsService, openCodeClient, { redactSecrets: true }))
Expand Down
4 changes: 3 additions & 1 deletion backend/test/routes/internal-assistant.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { describe, it, expect, beforeEach, vi } from 'vitest'
import type { SessionPermissionModeService } from '../../src/services/session-permission-modes'
import type { RepoWorkspaceService } from '../../src/services/repo-workspace'
import type { GitAuthService } from '../../src/services/git-auth'
import type { SessionGoalService } from '../../src/services/session-goals'
import type { MultiRunService } from '../../src/services/multi-runs'
import { Hono } from 'hono'
import { Database } from 'bun:sqlite'
import { createInternalRoutes } from '../../src/routes/internal'
Expand Down Expand Up @@ -46,7 +48,7 @@ describe('internal/assistant routes', () => {
notificationService = new NotificationService(db)
settingsService = new SettingsService(db)
app = new Hono()
app.route('/api/internal', createInternalRoutes(db, scheduleService, notificationService, settingsService, openCodeClient, {} as SessionPermissionModeService, {} as unknown as RepoWorkspaceService, {} as unknown as GitAuthService))
app.route('/api/internal', createInternalRoutes(db, scheduleService, notificationService, settingsService, openCodeClient, {} as SessionPermissionModeService, {} as unknown as RepoWorkspaceService, {} as unknown as GitAuthService, {} as unknown as SessionGoalService, {} as unknown as MultiRunService))
token = getOrCreateInternalToken(db)
})

Expand Down
123 changes: 123 additions & 0 deletions backend/test/routes/internal-multi-runs.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import { describe, it, expect, beforeEach, vi } from 'vitest'
import type { SessionPermissionModeService } from '../../src/services/session-permission-modes'
import type { RepoWorkspaceService } from '../../src/services/repo-workspace'
import type { GitAuthService } from '../../src/services/git-auth'
import type { SessionGoalService } from '../../src/services/session-goals'
import type { MultiRunService } from '../../src/services/multi-runs'
import { Hono } from 'hono'
import { Database } from 'bun:sqlite'
import { createInternalRoutes } from '../../src/routes/internal'
import type { ScheduleService } from '../../src/services/schedules'
import type { NotificationService } from '../../src/services/notification'
import type { SettingsService } from '../../src/services/settings'
import type { OpenCodeClient } from '../../src/services/opencode/client'
import { allMigrations } from '../../src/db/migrations'
import { getOrCreateInternalToken } from '../../src/services/internal-token'
import { migrate } from '../../src/db/migration-runner'

function createMultiRunStub() {
return {
list: vi.fn(),
launch: vi.fn(),
discard: vi.fn(),
fuse: vi.fn(),
}
}

function createRun(overrides: Record<string, unknown> = {}) {
return {
id: 1,
repoId: 1,
name: 'Sweep',
prompt: 'go',
isolated: true,
baseRef: null,
createdAt: 1,
entries: [],
fusions: [],
...overrides,
}
}

function fuseBody(overrides: Record<string, unknown> = {}) {
return {
requestId: '11111111-1111-4111-8111-111111111111',
entryIds: [1, 2],
model: 'openai/a',
...overrides,
}
}

describe('internal-multi-runs routes', () => {
let db: Database
let multiRuns: ReturnType<typeof createMultiRunStub>
let app: Hono
let token: string

beforeEach(() => {
db = new Database(':memory:')
migrate(db, allMigrations)
multiRuns = createMultiRunStub()
app = new Hono()
app.route(
'/api/internal',
createInternalRoutes(
db,
{} as ScheduleService,
{} as NotificationService,
{} as SettingsService,
{} as OpenCodeClient,
{} as SessionPermissionModeService,
{} as unknown as RepoWorkspaceService,
{} as unknown as GitAuthService,
{} as unknown as SessionGoalService,
multiRuns as unknown as MultiRunService,
),
)
token = getOrCreateInternalToken(db)
})

it('GET /api/internal/multi-runs returns 401 without bearer token', async () => {
const res = await app.request('/api/internal/multi-runs?repoId=1')
expect(res.status).toBe(401)
})

it('GET /api/internal/multi-runs returns the repository runs with a bearer token', async () => {
multiRuns.list.mockReturnValue([])

const res = await app.request('/api/internal/multi-runs?repoId=1', {
headers: { authorization: `Bearer ${token}` },
})

expect(res.status).toBe(200)
await expect(res.json()).resolves.toEqual({ runs: [] })
expect(multiRuns.list).toHaveBeenCalledWith(1)
})

it('POST /api/internal/multi-runs/3/entries/4/discard discards the entry', async () => {
multiRuns.discard.mockResolvedValue(createRun())

const res = await app.request('/api/internal/multi-runs/3/entries/4/discard', {
method: 'POST',
headers: { authorization: `Bearer ${token}` },
})

expect(res.status).toBe(200)
expect(multiRuns.discard).toHaveBeenCalledWith(3, 4)
})

it('POST /api/internal/multi-runs/3/fusions fuses the run', async () => {
multiRuns.fuse.mockResolvedValue({ run: createRun(), created: true })
const body = fuseBody()

const res = await app.request('/api/internal/multi-runs/3/fusions', {
method: 'POST',
headers: { authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
body: JSON.stringify(body),
})

expect(res.status).toBe(201)
await expect(res.json()).resolves.toEqual({ run: createRun() })
expect(multiRuns.fuse).toHaveBeenCalledWith(3, body)
})
})
4 changes: 3 additions & 1 deletion backend/test/routes/internal-notifications.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { describe, it, expect, beforeEach, vi } from 'bun:test'
import type { SessionPermissionModeService } from '../../src/services/session-permission-modes'
import type { RepoWorkspaceService } from '../../src/services/repo-workspace'
import type { GitAuthService } from '../../src/services/git-auth'
import type { SessionGoalService } from '../../src/services/session-goals'
import type { MultiRunService } from '../../src/services/multi-runs'
import { Hono } from 'hono'
import { Database } from 'bun:sqlite'
import { createInternalRoutes } from '../../src/routes/internal'
Expand Down Expand Up @@ -32,7 +34,7 @@ describe('internal/notifications routes', () => {
notificationService = new NotificationService(db)
settingsService = new SettingsService(db)
app = new Hono()
app.route('/api/internal', createInternalRoutes(db, scheduleService, notificationService, settingsService, openCodeClient, {} as SessionPermissionModeService, {} as unknown as RepoWorkspaceService, {} as unknown as GitAuthService))
app.route('/api/internal', createInternalRoutes(db, scheduleService, notificationService, settingsService, openCodeClient, {} as SessionPermissionModeService, {} as unknown as RepoWorkspaceService, {} as unknown as GitAuthService, {} as unknown as SessionGoalService, {} as unknown as MultiRunService))
token = getOrCreateInternalToken(db)
})

Expand Down
4 changes: 3 additions & 1 deletion backend/test/routes/internal-opencode-config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { describe, it, expect, beforeEach, afterEach, vi } from 'bun:test'
import type { SessionPermissionModeService } from '../../src/services/session-permission-modes'
import type { RepoWorkspaceService } from '../../src/services/repo-workspace'
import type { GitAuthService } from '../../src/services/git-auth'
import type { SessionGoalService } from '../../src/services/session-goals'
import type { MultiRunService } from '../../src/services/multi-runs'
import { Hono } from 'hono'
import { Database } from 'bun:sqlite'
import { readFile, writeFile } from 'fs/promises'
Expand Down Expand Up @@ -62,7 +64,7 @@ describe('internal/opencode-config routes', () => {
const notificationService = new NotificationService(db)
const settingsService = new SettingsService(db)
app = new Hono()
app.route('/api/internal', createInternalRoutes(db, scheduleService, notificationService, settingsService, openCodeClient, {} as SessionPermissionModeService, {} as unknown as RepoWorkspaceService, {} as unknown as GitAuthService))
app.route('/api/internal', createInternalRoutes(db, scheduleService, notificationService, settingsService, openCodeClient, {} as SessionPermissionModeService, {} as unknown as RepoWorkspaceService, {} as unknown as GitAuthService, {} as unknown as SessionGoalService, {} as unknown as MultiRunService))
token = getOrCreateInternalToken(db)
})

Expand Down
4 changes: 3 additions & 1 deletion backend/test/routes/internal-opencode-workspaces.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { describe, it, expect, beforeEach, vi } from 'vitest'
import type { SessionPermissionModeService } from '../../src/services/session-permission-modes'
import type { RepoWorkspaceService } from '../../src/services/repo-workspace'
import type { GitAuthService } from '../../src/services/git-auth'
import type { SessionGoalService } from '../../src/services/session-goals'
import type { MultiRunService } from '../../src/services/multi-runs'
import { Hono } from 'hono'
import type { Database } from 'bun:sqlite'
import { createInternalRoutes } from '../../src/routes/internal'
Expand Down Expand Up @@ -85,7 +87,7 @@ describe('internal-opencode-workspaces routes', () => {
forwardRaw: vi.fn(),
} as unknown as OpenCodeClient
app = new Hono()
app.route('/api/internal', createInternalRoutes(mockDb, scheduleService, notificationService, settingsService, openCodeClient, {} as SessionPermissionModeService, {} as unknown as RepoWorkspaceService, {} as unknown as GitAuthService))
app.route('/api/internal', createInternalRoutes(mockDb, scheduleService, notificationService, settingsService, openCodeClient, {} as SessionPermissionModeService, {} as unknown as RepoWorkspaceService, {} as unknown as GitAuthService, {} as unknown as SessionGoalService, {} as unknown as MultiRunService))
token = 'test-internal-token'
})

Expand Down
4 changes: 3 additions & 1 deletion backend/test/routes/internal-repos.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { describe, it, expect, beforeEach } from 'vitest'
import type { SessionPermissionModeService } from '../../src/services/session-permission-modes'
import type { RepoWorkspaceService } from '../../src/services/repo-workspace'
import type { GitAuthService } from '../../src/services/git-auth'
import type { SessionGoalService } from '../../src/services/session-goals'
import type { MultiRunService } from '../../src/services/multi-runs'
import { Hono } from 'hono'
import { Database } from 'bun:sqlite'
import { createInternalRoutes } from '../../src/routes/internal'
Expand Down Expand Up @@ -33,7 +35,7 @@ describe('internal-repos routes', () => {
notificationService = new NotificationService(db)
settingsService = new SettingsService(db)
app = new Hono()
app.route('/api/internal', createInternalRoutes(db, scheduleService, notificationService, settingsService, openCodeClient, {} as SessionPermissionModeService, {} as unknown as RepoWorkspaceService, {} as unknown as GitAuthService))
app.route('/api/internal', createInternalRoutes(db, scheduleService, notificationService, settingsService, openCodeClient, {} as SessionPermissionModeService, {} as unknown as RepoWorkspaceService, {} as unknown as GitAuthService, {} as unknown as SessionGoalService, {} as unknown as MultiRunService))
token = getOrCreateInternalToken(db)
})

Expand Down
4 changes: 3 additions & 1 deletion backend/test/routes/internal-sandbox.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { afterEach, beforeEach, describe, expect, it, mock, vi } from 'bun:test'
import type { SessionPermissionModeService } from '../../src/services/session-permission-modes'
import type { RepoWorkspaceService } from '../../src/services/repo-workspace'
import type { GitAuthService } from '../../src/services/git-auth'
import type { SessionGoalService } from '../../src/services/session-goals'
import type { MultiRunService } from '../../src/services/multi-runs'
import { Hono } from 'hono'
import { Database } from 'bun:sqlite'
import { mkdirSync, rmSync } from 'node:fs'
Expand Down Expand Up @@ -96,7 +98,7 @@ describe('internal sandbox routes', () => {
const notificationService = new NotificationService(db)
settingsService = new SettingsService(db)
app = new Hono()
app.route('/api/internal', createInternalRoutes(db, scheduleService, notificationService, settingsService, openCodeClient, {} as SessionPermissionModeService, {} as unknown as RepoWorkspaceService, {} as unknown as GitAuthService))
app.route('/api/internal', createInternalRoutes(db, scheduleService, notificationService, settingsService, openCodeClient, {} as SessionPermissionModeService, {} as unknown as RepoWorkspaceService, {} as unknown as GitAuthService, {} as unknown as SessionGoalService, {} as unknown as MultiRunService))
token = getOrCreateInternalToken(db)
repoDir = path.join(getReposPath(), 'sandbox-route-test')
mkdirSync(repoDir, { recursive: true })
Expand Down
4 changes: 3 additions & 1 deletion backend/test/routes/internal-schedules.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { describe, it, expect, beforeEach } from 'vitest'
import type { SessionPermissionModeService } from '../../src/services/session-permission-modes'
import type { RepoWorkspaceService } from '../../src/services/repo-workspace'
import type { GitAuthService } from '../../src/services/git-auth'
import type { SessionGoalService } from '../../src/services/session-goals'
import type { MultiRunService } from '../../src/services/multi-runs'
import { Hono } from 'hono'
import { Database } from 'bun:sqlite'
import { createInternalRoutes } from '../../src/routes/internal'
Expand Down Expand Up @@ -31,7 +33,7 @@ describe('internal-schedules routes', () => {
notificationService = new NotificationService(db)
settingsService = new SettingsService(db)
app = new Hono()
app.route('/api/internal', createInternalRoutes(db, scheduleService, notificationService, settingsService, openCodeClient, {} as SessionPermissionModeService, {} as unknown as RepoWorkspaceService, {} as unknown as GitAuthService))
app.route('/api/internal', createInternalRoutes(db, scheduleService, notificationService, settingsService, openCodeClient, {} as SessionPermissionModeService, {} as unknown as RepoWorkspaceService, {} as unknown as GitAuthService, {} as unknown as SessionGoalService, {} as unknown as MultiRunService))
token = getOrCreateInternalToken(db)
})

Expand Down
101 changes: 101 additions & 0 deletions backend/test/routes/internal-session-goals.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { describe, it, expect, beforeEach, vi } from 'vitest'
import type { SessionPermissionModeService } from '../../src/services/session-permission-modes'
import type { RepoWorkspaceService } from '../../src/services/repo-workspace'
import type { GitAuthService } from '../../src/services/git-auth'
import type { SessionGoalService } from '../../src/services/session-goals'
import type { MultiRunService } from '../../src/services/multi-runs'
import { Hono } from 'hono'
import { Database } from 'bun:sqlite'
import { createInternalRoutes } from '../../src/routes/internal'
import type { ScheduleService } from '../../src/services/schedules'
import type { NotificationService } from '../../src/services/notification'
import type { SettingsService } from '../../src/services/settings'
import type { OpenCodeClient } from '../../src/services/opencode/client'
import { allMigrations } from '../../src/db/migrations'
import { getOrCreateInternalToken } from '../../src/services/internal-token'
import { migrate } from '../../src/db/migration-runner'

function createSessionGoalStub() {
return {
getLatest: vi.fn(),
start: vi.fn(),
pause: vi.fn(),
resume: vi.fn(),
cancel: vi.fn(),
}
}

describe('internal-session-goals routes', () => {
let db: Database
let sessionGoals: ReturnType<typeof createSessionGoalStub>
let app: Hono
let token: string

beforeEach(() => {
db = new Database(':memory:')
migrate(db, allMigrations)
sessionGoals = createSessionGoalStub()
app = new Hono()
app.route(
'/api/internal',
createInternalRoutes(
db,
{} as ScheduleService,
{} as NotificationService,
{} as SettingsService,
{} as OpenCodeClient,
{} as SessionPermissionModeService,
{} as unknown as RepoWorkspaceService,
{} as unknown as GitAuthService,
sessionGoals as unknown as SessionGoalService,
{} as unknown as MultiRunService,
),
)
token = getOrCreateInternalToken(db)
})

it('GET /api/internal/session-goals returns 401 without bearer token', async () => {
const res = await app.request('/api/internal/session-goals?sessionId=ses_1')
expect(res.status).toBe(401)
})

it('GET /api/internal/session-goals returns the latest goal with a bearer token', async () => {
sessionGoals.getLatest.mockReturnValue(null)

const res = await app.request('/api/internal/session-goals?sessionId=ses_1', {
headers: { authorization: `Bearer ${token}` },
})

expect(res.status).toBe(200)
await expect(res.json()).resolves.toEqual({ goal: null })
expect(sessionGoals.getLatest).toHaveBeenCalledWith('ses_1')
})

it('POST /api/internal/session-goals starts a goal', async () => {
sessionGoals.start.mockResolvedValue({ id: 1, status: 'active' })
const body = { sessionId: 'ses_1', directory: '/abs/repo', objective: 'Ship it' }

const res = await app.request('/api/internal/session-goals', {
method: 'POST',
headers: { authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
body: JSON.stringify(body),
})

expect(res.status).toBe(201)
await expect(res.json()).resolves.toEqual({ goal: { id: 1, status: 'active' } })
expect(sessionGoals.start).toHaveBeenCalledWith(body)
})

it('POST /api/internal/session-goals/7/pause pauses the goal', async () => {
sessionGoals.pause.mockReturnValue({ id: 7, status: 'paused' })

const res = await app.request('/api/internal/session-goals/7/pause', {
method: 'POST',
headers: { authorization: `Bearer ${token}` },
})

expect(res.status).toBe(200)
await expect(res.json()).resolves.toEqual({ goal: { id: 7, status: 'paused' } })
expect(sessionGoals.pause).toHaveBeenCalledWith(7)
})
})
Loading