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
6 changes: 6 additions & 0 deletions backend/eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,10 @@ export default defineConfig([
'no-useless-escape': 'warn',
},
},
{
files: ['test/**/*.ts'],
rules: {
'@typescript-eslint/no-explicit-any': 'off',
},
},
])
3 changes: 2 additions & 1 deletion backend/src/db/migrations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,8 @@ function migrateGitTokenToCredentials(db: Database): void {
continue
}

const { gitToken: _, ...rest } = parsed
const { gitToken: _gitToken, ...rest } = parsed
void _gitToken
const migrated = {
...rest,
gitCredentials: [{
Expand Down
2 changes: 1 addition & 1 deletion backend/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ try {
logger.error('Failed to initialize workspace:', error)
}

app.route('/api/auth', createAuthRoutes(auth, db))
app.route('/api/auth', createAuthRoutes(auth))
app.route('/api/auth-info', createAuthInfoRoutes(auth, db))

app.route('/api/health', createHealthRoutes(db))
Expand Down
2 changes: 1 addition & 1 deletion backend/src/routes/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { ENV } from '@opencode-manager/shared/config/env'
import { logger } from '../utils/logger'
import { hashPassword } from 'better-auth/crypto'

export function createAuthRoutes(auth: AuthInstance, _db: Database) {
export function createAuthRoutes(auth: AuthInstance): Hono {
const app = new Hono()

app.all('/*', async (c) => {
Expand Down
40 changes: 24 additions & 16 deletions backend/src/services/archive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ import { readdir, stat, unlink } from 'fs/promises'
import path from 'path'
import os from 'os'
import { logger } from '../utils/logger'
import { getReposPath } from '@opencode-manager/shared/config/env'

function resolvePath(userPath: string): string {
return path.isAbsolute(userPath) ? userPath : path.join(getReposPath(), userPath)
}

export interface ArchiveOptions {
includeGit?: boolean
Expand Down Expand Up @@ -57,14 +62,14 @@ async function getIgnoredPaths(gitRoot: string, targetPath: string, paths: strin
const { spawn } = await import('child_process')

const targetRelativeToRoot = path.relative(gitRoot, targetPath)
console.log('[getIgnoredPaths] gitRoot:', gitRoot, 'targetPath:', targetPath, 'targetRelativeToRoot:', targetRelativeToRoot)
logger.debug('[getIgnoredPaths] gitRoot:', gitRoot, 'targetPath:', targetPath, 'targetRelativeToRoot:', targetRelativeToRoot)

const relativePaths = paths.map(p => {
const relativeToTarget = p
return targetRelativeToRoot ? path.join(targetRelativeToRoot, relativeToTarget) : relativeToTarget
})

console.log('[getIgnoredPaths] First 5 relativePaths:', relativePaths.slice(0, 5))
logger.debug('[getIgnoredPaths] First 5 relativePaths:', relativePaths.slice(0, 5))

return new Promise((resolve) => {
const ignored = new Set<string>()
Expand All @@ -83,9 +88,9 @@ async function getIgnoredPaths(gitRoot: string, targetPath: string, paths: strin
proc.stdin?.end()

proc.on('close', (code) => {
console.log('[getIgnoredPaths] git check-ignore exited with code:', code)
logger.debug('[getIgnoredPaths] git check-ignore exited with code:', code)
const ignoredFullPaths = stdout.split('\n').filter(p => p.trim())
console.log('[getIgnoredPaths] Raw ignored count:', ignoredFullPaths.length, 'first 5:', ignoredFullPaths.slice(0, 5))
logger.debug('[getIgnoredPaths] Raw ignored count:', ignoredFullPaths.length, 'first 5:', ignoredFullPaths.slice(0, 5))
const targetRelativeToRoot = path.relative(gitRoot, targetPath)

for (const fullPath of ignoredFullPaths) {
Expand All @@ -99,7 +104,7 @@ async function getIgnoredPaths(gitRoot: string, targetPath: string, paths: strin
ignored.add(relativePath)
}
}
console.log('[getIgnoredPaths] Final ignored set size:', ignored.size)
logger.debug('[getIgnoredPaths] Final ignored set size:', ignored.size)
resolve(ignored)
})

Expand Down Expand Up @@ -200,6 +205,7 @@ async function filterIgnoredPaths(targetPath: string, allPaths: string[], option
}

export async function createRepoArchive(repoPath: string, options?: ArchiveOptions): Promise<string> {
repoPath = resolvePath(repoPath)
const repoName = path.basename(repoPath)
const tempFile = path.join(os.tmpdir(), `${repoName}-${Date.now()}.zip`)

Expand Down Expand Up @@ -237,6 +243,7 @@ export async function createRepoArchive(repoPath: string, options?: ArchiveOptio
}

export async function createDirectoryArchive(directoryPath: string, archiveName?: string, options?: ArchiveOptions): Promise<string> {
directoryPath = resolvePath(directoryPath)
const dirName = archiveName || path.basename(directoryPath)
const tempFile = path.join(os.tmpdir(), `${dirName}-${Date.now()}.zip`)

Expand Down Expand Up @@ -293,34 +300,35 @@ export async function getArchiveSize(filePath: string): Promise<number> {
}

export async function getIgnoredPathsList(directoryPath: string): Promise<string[]> {
console.log('[getIgnoredPathsList] Starting for:', directoryPath)
directoryPath = resolvePath(directoryPath)
logger.debug('[getIgnoredPathsList] Starting for:', directoryPath)
const gitRoot = await findGitRoot(directoryPath)
console.log('[getIgnoredPathsList] Git root:', gitRoot)
logger.debug('[getIgnoredPathsList] Git root:', gitRoot)

if (!gitRoot) {
console.log('[getIgnoredPathsList] No git root found')
logger.debug('[getIgnoredPathsList] No git root found')
const hasGitDir = await collectFiles(directoryPath).then(
paths => paths.some(p => p.startsWith('.git/') || p === '.git')
).catch(() => false)

if (hasGitDir) {
console.log('[getIgnoredPathsList] Has .git dir, returning [.git/]')
logger.debug('[getIgnoredPathsList] Has .git dir, returning [.git/]')
return ['.git/']
}
console.log('[getIgnoredPathsList] No .git dir, returning []')
logger.debug('[getIgnoredPathsList] No .git dir, returning []')
return []
}

const allPaths = await collectFiles(directoryPath)
console.log('[getIgnoredPathsList] Collected', allPaths.length, 'paths')
logger.debug('[getIgnoredPathsList] Collected', allPaths.length, 'paths')
const ignoredSet = new Set<string>()
const batchSize = 1000

for (let i = 0; i < allPaths.length; i += batchSize) {
const batch = allPaths.slice(i, i + batchSize)
console.log('[getIgnoredPathsList] Checking batch', i, 'to', i + batch.length)
logger.debug('[getIgnoredPathsList] Checking batch', i, 'to', i + batch.length)
const ignored = await getIgnoredPaths(gitRoot, directoryPath, batch)
console.log('[getIgnoredPathsList] Batch ignored count:', ignored.size)
logger.debug('[getIgnoredPathsList] Batch ignored count:', ignored.size)
for (const p of ignored) {
ignoredSet.add(p)
if (p.endsWith('/')) {
Expand All @@ -331,7 +339,7 @@ export async function getIgnoredPathsList(directoryPath: string): Promise<string
}
}

console.log('[getIgnoredPathsList] Total ignored set size:', ignoredSet.size)
logger.debug('[getIgnoredPathsList] Total ignored set size:', ignoredSet.size)

const ignoredDirs: string[] = []
const processedDirs = new Set<string>()
Expand All @@ -358,14 +366,14 @@ export async function getIgnoredPathsList(directoryPath: string): Promise<string
}

const gitDirExists = await stat(path.join(directoryPath, '.git')).then(() => true).catch(() => false)
console.log('[getIgnoredPathsList] .git exists:', gitDirExists)
logger.debug('[getIgnoredPathsList] .git exists:', gitDirExists)
if (gitDirExists && !ignoredDirs.some(p => p.startsWith('.git'))) {
ignoredDirs.push('.git/')
}

ignoredDirs.sort()

console.log('[getIgnoredPathsList] Final result:', ignoredDirs)
logger.debug('[getIgnoredPathsList] Final result:', ignoredDirs)

return ignoredDirs
}
15 changes: 2 additions & 13 deletions backend/test/routes/files.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Hono } from 'hono'
import type { ReadStream } from 'fs'
import * as fileService from '../../src/services/files'
import * as archiveService from '../../src/services/archive'
import type { FileInfo, ChunkedFileInfo, PatchOperation } from '@opencode-manager/shared'
import type { FileInfo, ChunkedFileInfo } from '@opencode-manager/shared'

interface FileUploadResult {
name: string
Expand Down Expand Up @@ -47,7 +47,6 @@ vi.mock('../../src/services/archive', () => ({
}))

const getFile = fileService.getFile as MockedFunction<typeof fileService.getFile>
const getRawFileContent = fileService.getRawFileContent as MockedFunction<typeof fileService.getRawFileContent>
const getFileRange = fileService.getFileRange as MockedFunction<typeof fileService.getFileRange>
const uploadFile = fileService.uploadFile as MockedFunction<typeof fileService.uploadFile>
const createFileOrFolder = fileService.createFileOrFolder as MockedFunction<typeof fileService.createFileOrFolder>
Expand All @@ -58,7 +57,6 @@ const applyFilePatches = fileService.applyFilePatches as MockedFunction<typeof f
const createDirectoryArchive = archiveService.createDirectoryArchive as MockedFunction<typeof archiveService.createDirectoryArchive>
const getArchiveSize = archiveService.getArchiveSize as MockedFunction<typeof archiveService.getArchiveSize>
const getArchiveStream = archiveService.getArchiveStream as MockedFunction<typeof archiveService.getArchiveStream>
const deleteArchive = archiveService.deleteArchive as MockedFunction<typeof archiveService.deleteArchive>

describe('File Routes', () => {
let app: Hono
Expand All @@ -81,7 +79,7 @@ describe('File Routes', () => {
getArchiveSize.mockResolvedValue(1024)
getArchiveStream.mockReturnValue(mockStream)

const response = await app.request('/api/files/test-repo/src/download-zip')
await app.request('/api/files/test-repo/src/download-zip')

expect(createDirectoryArchive).toHaveBeenCalledWith('test-repo/src', undefined, {
includeGit: false,
Expand Down Expand Up @@ -497,15 +495,6 @@ describe('File Routes', () => {
})

describe('Path Traversal Protection', () => {
const mockFileInfo: FileInfo = {
name: 'test.txt',
path: 'safe-path/test.txt',
isDirectory: false,
size: 100,
mimeType: 'text/plain',
content: '',
lastModified: new Date(),
}

it('should reject path with ../ segments via service error', async () => {
const error = { message: 'Path traversal detected', statusCode: 403 }
Expand Down
21 changes: 1 addition & 20 deletions backend/test/routes/tts.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import * as fs from 'fs/promises'
import { logger } from '../../src/utils/logger'

vi.mock('fs/promises', () => ({
mkdir: vi.fn(),
Expand All @@ -27,7 +26,6 @@ vi.mock('../../src/utils/logger', () => ({

const mockMkdir = fs.mkdir as any
const mockReadFile = fs.readFile as any
const mockWriteFile = fs.writeFile as any
const mockReaddir = fs.readdir as any
const mockStat = fs.stat as any
const mockUnlink = fs.unlink as any
Expand All @@ -36,29 +34,12 @@ import { createTTSRoutes, cleanupExpiredCache, getCacheStats, generateCacheKey,

describe('TTS Routes', () => {
let mockDb: any
let ttsApp: any
let mockSettingsService: any

beforeEach(() => {
vi.clearAllMocks()

mockDb = {} as any
mockSettingsService = {
getSettings: vi.fn().mockReturnValue({
preferences: {
tts: {
enabled: true,
apiKey: 'test-key',
endpoint: 'https://api.openai.com/v1/audio/speech',
voice: 'alloy',
model: 'tts-1',
speed: 1.0,
},
},
}),
}

ttsApp = createTTSRoutes(mockDb)
createTTSRoutes(mockDb)
})

describe('generateCacheKey', () => {
Expand Down
2 changes: 0 additions & 2 deletions backend/test/services/git/GitBranchService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,12 +218,10 @@ describe('GitBranchService', () => {
it('handles getBranchStatus failure for current branch gracefully', async () => {
const mockRepo = { id: 1, fullPath: '/path/to/repo' }
getRepoByIdMock.mockReturnValue(mockRepo as any)
let revListCalls = 0
executeCommandMock.mockImplementation((args) => {
if (args.includes('rev-parse')) return Promise.resolve('main')
if (args.includes('branch')) return Promise.resolve('* main abc123 Initial commit')
if (args.includes('rev-list')) {
revListCalls++
return Promise.reject(new Error('No upstream'))
}
return Promise.resolve('')
Expand Down
1 change: 0 additions & 1 deletion backend/test/services/git/GitLogService.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { describe, it, expect, vi, beforeEach, type MockedFunction } from 'vitest'

vi.mock('bun:sqlite', () => ({
Expand Down
13 changes: 0 additions & 13 deletions backend/test/utils/helpers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,24 @@ import { describe, it, expect } from 'vitest'
describe('Process Helper Functions', () => {
describe('mapProcessState', () => {
it('should map "running" state correctly', () => {
const state = 'running'
const mapped = 'running' as const

expect(mapped).toBe('running')
})

it('should map "stopped" state to "stopped"', () => {
const state = 'stopped'
const mapped = 'stopped' as const

expect(mapped).toBe('stopped')
})

it('should map "starting" state to "starting"', () => {
const state = 'starting'
const mapped = 'starting' as const

expect(mapped).toBe('starting')
})

it('should map unknown states to "error"', () => {
const state = 'unknown'
const mapped = 'error' as const

expect(mapped).toBe('error')
Expand Down Expand Up @@ -62,28 +58,24 @@ describe('Process Helper Functions', () => {

describe('validateRepoUrl', () => {
it('should accept valid GitHub HTTPS URLs', () => {
const url = 'https://github.com/user/repo'
const isValid = true

expect(isValid).toBe(true)
})

it('should accept valid GitHub SSH URLs', () => {
const url = 'git@github.com:user/repo.git'
const isValid = true

expect(isValid).toBe(true)
})

it('should reject invalid URLs', () => {
const url = 'not-a-url'
const isValid = false

expect(isValid).toBe(false)
})

it('should accept GitLab URLs', () => {
const url = 'https://gitlab.com/user/repo'
const isValid = true

expect(isValid).toBe(true)
Expand All @@ -92,11 +84,6 @@ describe('Process Helper Functions', () => {

describe('sanitizeEnvVars', () => {
it('should filter out undefined values', () => {
const env = {
DEFINED: 'value',
UNDEFINED: undefined
}

const sanitized = ['DEFINED=value']

expect(sanitized).not.toContain('UNDEFINED')
Expand Down
Loading