diff --git a/opencode.json b/opencode.json index fb5f117..983520b 100644 --- a/opencode.json +++ b/opencode.json @@ -1,6 +1,5 @@ { "$schema": "https://opencode.ai/config.json", - "model": "anthropic/claude-sonnet-4-20250514", "instructions": [ "AGENTS.local.md" ], diff --git a/src/cli/types.ts b/src/cli/types.ts index 647997f..ca59a48 100644 --- a/src/cli/types.ts +++ b/src/cli/types.ts @@ -49,7 +49,8 @@ export const EXIT_CODES = { INVALID_ARGUMENTS: 2, GIT_REPO_NOT_FOUND: 3, NETWORK_ERROR: 4, - FILESYSTEM_ERROR: 5 + FILESYSTEM_ERROR: 5, + GIT_ERROR: 6 } as const; export type ExitCode = (typeof EXIT_CODES)[keyof typeof EXIT_CODES]; diff --git a/src/init.ts b/src/init.ts index 5ec4440..2b7a665 100644 --- a/src/init.ts +++ b/src/init.ts @@ -8,6 +8,8 @@ import { EXIT_CODES } from './cli/types.ts'; import type { RepositoryInfo } from './repository.ts'; import type { ServiceContainer } from './services/types.ts'; import { createServiceContainer } from './services/container.ts'; +import { WorktreeOperations } from './worktree.ts'; +import { loadConfig } from './config.ts'; export class RepositoryInitError extends Error { constructor(message: string, public readonly code: number = EXIT_CODES.GENERAL_ERROR) { @@ -120,7 +122,7 @@ export class InitOperations { // Validate targetName characters if (trimmedTarget.includes('/') || trimmedTarget.includes('\\') || trimmedTarget.includes('@')) { - throw new RepositoryInitError('Invalid repository name: contains invalid characters', EXIT_CODES.INVALID_ARGUMENTS); + throw new RepositoryInitError('Repository name: contains invalid characters', EXIT_CODES.INVALID_ARGUMENTS); } repoName = trimmedTarget; @@ -157,15 +159,21 @@ export class InitOperations { this.services.logger.log('Fetching all remote branches...'); await this.fetchAllRemoteBranches(bareDir); - this.services.logger.log(`Repository initialized successfully in ${targetDir}`); - - return { + const repoInfo: RepositoryInfo = { rootDir: targetDir, gitDir: bareDir, type: 'bare', bareDir: bareDir }; + // Detect and create default branch worktree + this.services.logger.log('Creating default branch worktree...'); + await this.createDefaultBranchWorktree(repoInfo); + + this.services.logger.log(`Repository initialized successfully in ${targetDir}`); + + return repoInfo; + } catch (error) { if (error instanceof GitError) { // Check if it's a network-related error @@ -228,6 +236,109 @@ export class InitOperations { throw new GitError(`Failed to fetch branches: ${error instanceof Error ? error.message : 'Unknown error'}`, '', -1); } } + + /** + * Detects the default branch from the bare repository's HEAD file + */ + private async detectDefaultBranch(bareDir: string): Promise { + const headFilePath = join(bareDir, 'HEAD'); + + try { + const headContent = await this.services.fs.readFile(headFilePath, 'utf8'); + const headLine = headContent.trim(); + + // HEAD file should contain something like "ref: refs/heads/main" + const match = headLine.match(/^ref:\s*refs\/heads\/(.+)$/); + if (match && match[1]) { + return match[1]; + } + + // Fallback: try to detect from remote HEAD + const result = await this.services.git.executeCommandWithResult(bareDir, ['symbolic-ref', 'refs/remotes/origin/HEAD']); + if (result.exitCode === 0) { + const remoteHeadMatch = result.stdout.trim().match(/refs\/remotes\/origin\/(.+)$/); + if (remoteHeadMatch && remoteHeadMatch[1]) { + return remoteHeadMatch[1]; + } + } + + // Last resort fallback + throw new Error('Could not determine default branch'); + } catch (error) { + // If we can't detect the default branch, try common defaults + const commonDefaults = ['main', 'master', 'develop', 'development']; + + for (const branch of commonDefaults) { + try { + const result = await this.services.git.executeCommandWithResult(bareDir, ['show-ref', '--verify', '--quiet', `refs/remotes/origin/${branch}`]); + if (result.exitCode === 0) { + this.services.logger.warn(`Could not detect default branch from HEAD file, using found branch: ${branch}`); + return branch; + } + } catch { + // Continue trying other branches + } + } + + throw new RepositoryInitError( + `Could not determine default branch: ${error instanceof Error ? error.message : 'Unknown error'}. ` + + 'Make sure the repository has a valid default branch.', + EXIT_CODES.GIT_ERROR + ); + } + } + + /** + * Creates a worktree for the default branch and sets up upstream tracking + */ + private async createDefaultBranchWorktree(repoInfo: RepositoryInfo): Promise { + try { + // Detect the default branch + const defaultBranch = await this.detectDefaultBranch(repoInfo.gitDir); + this.services.logger.log(`Detected default branch: ${defaultBranch}`); + + // Load config to get worktree directory setting + const config = await loadConfig(repoInfo); + + // Create worktree for the default branch + const worktreeOps = new WorktreeOperations(this.services); + + // Change to the repository directory temporarily to ensure relative paths work correctly + const originalCwd = process.cwd(); + this.services.fs.chdir(repoInfo.rootDir); + + try { + // Resolve branch and create worktree + const resolution = await worktreeOps.resolveBranch(repoInfo, defaultBranch, config); + const worktreePath = join(repoInfo.rootDir, config.worktreeDir, defaultBranch); + + await worktreeOps.createWorktree(repoInfo, resolution, worktreePath); + + // Set up upstream tracking for the created branch + await this.setupUpstreamTracking(repoInfo, defaultBranch, worktreePath); + + this.services.logger.log(`Created worktree for default branch '${defaultBranch}' with upstream tracking`); + } finally { + // Restore original working directory + this.services.fs.chdir(originalCwd); + } + } catch (error) { + // Don't fail the entire init process if worktree creation fails + this.services.logger.warn(`Warning: Failed to create default branch worktree: ${error instanceof Error ? error.message : 'Unknown error'}`); + } + } + + /** + * Sets up upstream tracking for a worktree branch + */ + private async setupUpstreamTracking(repoInfo: RepositoryInfo, branchName: string, worktreePath: string): Promise { + try { + // Set the upstream branch to track origin/branchName + await this.services.git.executeCommandInDir(worktreePath, ['branch', '--set-upstream-to', `origin/${branchName}`]); + } catch (error) { + this.services.logger.warn(`Warning: Failed to set upstream tracking for branch '${branchName}': ${error instanceof Error ? error.message : 'Unknown error'}`); + } + } } /** diff --git a/src/services/implementations/NodeFileSystemService.ts b/src/services/implementations/NodeFileSystemService.ts index d565804..ee15641 100644 --- a/src/services/implementations/NodeFileSystemService.ts +++ b/src/services/implementations/NodeFileSystemService.ts @@ -60,4 +60,8 @@ export class NodeFileSystemService implements FileSystemService { return false; } } + + chdir(path: string): void { + process.chdir(path); + } } \ No newline at end of file diff --git a/src/services/test-implementations/MockFileSystemService.ts b/src/services/test-implementations/MockFileSystemService.ts index da2e855..559bf3c 100644 --- a/src/services/test-implementations/MockFileSystemService.ts +++ b/src/services/test-implementations/MockFileSystemService.ts @@ -15,6 +15,9 @@ export class MockFileSystemService implements FileSystemService { private directories = new Set(); private accessiblePaths = new Set(); private mockStats = new Map(); + private currentDirectory = '/'; + private fileWrites: Array<{path: string, data: string, encoding?: BufferEncoding}> = []; + private chdirCalls: string[] = []; // Configuration methods for tests setFileContent(path: string, content: string): void { @@ -47,13 +50,22 @@ export class MockFileSystemService implements FileSystemService { this.directories.clear(); this.accessiblePaths.clear(); this.mockStats.clear(); + this.currentDirectory = '/'; + this.fileWrites = []; + this.chdirCalls = []; } getFileWrites(): Array<{path: string, data: string, encoding?: BufferEncoding}> { return [...this.fileWrites]; } - private fileWrites: Array<{path: string, data: string, encoding?: BufferEncoding}> = []; + getChdirCalls(): string[] { + return [...this.chdirCalls]; + } + + getCurrentDirectory(): string { + return this.currentDirectory; + } // FileSystemService implementation async readFile(path: string, _encoding: BufferEncoding = 'utf-8'): Promise { @@ -121,4 +133,9 @@ export class MockFileSystemService implements FileSystemService { return false; } } + + chdir(path: string): void { + this.chdirCalls.push(path); + this.currentDirectory = path; + } } \ No newline at end of file diff --git a/src/services/test-implementations/MockLoggerService.ts b/src/services/test-implementations/MockLoggerService.ts index 46c13ec..b6ff982 100644 --- a/src/services/test-implementations/MockLoggerService.ts +++ b/src/services/test-implementations/MockLoggerService.ts @@ -44,4 +44,8 @@ export class MockLoggerService implements LoggerService { hasLog(level: string, message: string): boolean { return this.logs.some(log => log.level === level && log.message === message); } + + hasLogContaining(level: string, messageSubstring: string): boolean { + return this.logs.some(log => log.level === level && log.message.includes(messageSubstring)); + } } \ No newline at end of file diff --git a/src/services/types.ts b/src/services/types.ts index b785aba..733a0c7 100644 --- a/src/services/types.ts +++ b/src/services/types.ts @@ -33,6 +33,7 @@ export interface FileSystemService { exists(path: string): Promise; isDirectory(path: string): Promise; isFile(path: string): Promise; + chdir(path: string): void; } export interface CommandService { diff --git a/tests/integration/init.test.ts b/tests/integration/init.test.ts index 4ddc360..bcbc3bb 100644 --- a/tests/integration/init.test.ts +++ b/tests/integration/init.test.ts @@ -219,6 +219,126 @@ test('integration: initialized repository should be detected correctly', async ( expect(stdout).toContain('.bare'); }); +test('integration: init command should create default branch worktree automatically', async () => { + const targetName = 'default-branch-test'; + + // Initialize repository + const { stdout, exitCode } = await runWt(`init ${testRepoUrl} ${targetName}`, tempDir); + + expect(exitCode).toBe(0); + expect(stdout).toContain('Creating default branch worktree'); + expect(stdout).toContain('Detected default branch: main'); + expect(stdout).toContain('Created worktree for default branch'); + + // Verify worktree was created + const targetPath = path.join(tempDir, targetName); + const { stdout: listOutput, exitCode: listExitCode } = await runWt('list', targetPath); + + expect(listExitCode).toBe(0); + expect(listOutput).toContain('main'); + + // Verify the worktree directory exists + const mainWorktreePath = path.join(targetPath, 'main'); + try { + await access(mainWorktreePath, constants.F_OK); + const worktreStat = await stat(mainWorktreePath); + expect(worktreStat.isDirectory()).toBe(true); + } catch { + throw new Error(`Expected main worktree directory to exist at ${mainWorktreePath}`); + } + + // Verify the README.md file is present in the worktree + const readmePath = path.join(mainWorktreePath, 'README.md'); + try { + await access(readmePath, constants.F_OK); + const readmeContent = await readFile(readmePath, 'utf-8'); + expect(readmeContent).toContain('test-repo'); + } catch { + throw new Error(`Expected README.md to exist in main worktree at ${readmePath}`); + } +}); + +test('integration: default branch worktree should have upstream tracking', async () => { + const targetName = 'upstream-test'; + + // Initialize repository + const { exitCode } = await runWt(`init ${testRepoUrl} ${targetName}`, tempDir); + expect(exitCode).toBe(0); + + // Check upstream tracking is set up + const mainWorktreePath = path.join(tempDir, targetName, 'main'); + + // Verify upstream is configured + const { stdout: upstreamOutput } = await execAsync('git branch -vv', { cwd: mainWorktreePath }); + expect(upstreamOutput).toContain('origin/main'); +}); + +test('integration: init should handle repositories with different default branches', async () => { + // Create a test repository with 'master' as default branch + const masterRepoPath = path.join(tempDir, 'master-repo.git'); + await execAsync(`git init --bare "${masterRepoPath}"`); + + // Set the default branch to master in the bare repo + await execAsync(`git symbolic-ref HEAD refs/heads/master`, { cwd: masterRepoPath }); + + // Create working directory and set up with master branch + const workingDir = path.join(tempDir, 'master-working'); + await execAsync(`git clone "${masterRepoPath}" "${workingDir}"`); + + // Configure git user + await execAsync('git config user.name "Test User"', { cwd: workingDir }); + await execAsync('git config user.email "test@example.com"', { cwd: workingDir }); + + // Create initial commit on master branch + await writeFile(path.join(workingDir, 'README.md'), '# Master repo\n'); + await execAsync('git add README.md', { cwd: workingDir }); + await execAsync('git commit -m "Initial commit"', { cwd: workingDir }); + await execAsync('git push origin master', { cwd: workingDir }); + + // Clean up working directory + await rm(workingDir, { recursive: true, force: true }); + + const masterRepoUrl = `file://${masterRepoPath}`; + const targetName = 'master-branch-test'; + + // Initialize repository with master default branch + const { stdout, exitCode } = await runWt(`init ${masterRepoUrl} ${targetName}`, tempDir); + + expect(exitCode).toBe(0); + expect(stdout).toContain('Detected default branch: master'); + expect(stdout).toContain('Created worktree for default branch \'master\''); + + // Verify master worktree was created + const targetPath = path.join(tempDir, targetName); + const { stdout: listOutput } = await runWt('list', targetPath); + expect(listOutput).toContain('master'); + + const masterWorktreePath = path.join(targetPath, 'master'); + await access(masterWorktreePath, constants.F_OK); +}); + +test('integration: init should warn but continue if default branch worktree creation fails', async () => { + // Create a repository with no commits (which might cause worktree creation to fail) + const emptyRepoPath = path.join(tempDir, 'empty-repo.git'); + await execAsync(`git init --bare "${emptyRepoPath}"`); + + const emptyRepoUrl = `file://${emptyRepoPath}`; + const targetName = 'empty-repo-test'; + + const { stdout, stderr, exitCode } = await runWt(`init ${emptyRepoUrl} ${targetName}`, tempDir); + + // Should still succeed overall even if worktree creation fails + expect(exitCode).toBe(0); + expect(stdout).toContain('Repository initialized successfully'); + + // Should show warning about worktree creation failure + if (stderr.includes('Warning: Failed to create default branch worktree') || + stdout.includes('Warning: Failed to create default branch worktree')) { + // This is expected for empty repositories + expect(true).toBe(true); + } +}); + test('integration: init command should handle existing directory name conflicts', async () => { const targetName = 'conflict-test'; diff --git a/tests/unit/init.test.ts b/tests/unit/init.test.ts index cc25ac3..10b2818 100644 --- a/tests/unit/init.test.ts +++ b/tests/unit/init.test.ts @@ -1,6 +1,9 @@ import { test, expect } from 'bun:test'; -import { validateAndParseGitUrl, RepositoryInitError, NetworkError } from '../../src/init.ts'; +import { resolve, join } from 'path'; +import { validateAndParseGitUrl, RepositoryInitError, NetworkError, InitOperations } from '../../src/init.ts'; import { EXIT_CODES } from '../../src/cli/types.ts'; +import { MockLoggerService, MockGitService, MockFileSystemService, MockCommandService } from '../../src/services/test-implementations/index.ts'; +import { createServiceContainer } from '../../src/services/container.ts'; // Test validateAndParseGitUrl function test('validateAndParseGitUrl - HTTP URL', () => { @@ -107,4 +110,223 @@ test('NetworkError - inherits from RepositoryInitError', () => { expect(error.code).toBe(EXIT_CODES.NETWORK_ERROR); expect(error).toBeInstanceOf(RepositoryInitError); expect(error).toBeInstanceOf(Error); +}); + +// Test InitOperations class with service injection +test('InitOperations - default branch detection from HEAD file', async () => { + const mockLogger = new MockLoggerService(); + const mockGit = new MockGitService(); + const mockFs = new MockFileSystemService(); + const mockCmd = new MockCommandService(); + + const services = createServiceContainer({ + logger: mockLogger, + git: mockGit, + fs: mockFs, + cmd: mockCmd + }); + + const initOps = new InitOperations(services); + + // Resolve expected paths based on current working directory + const expectedTargetDir = resolve(process.cwd(), 'repo'); + const expectedBareDir = join(expectedTargetDir, '.bare'); + + // Mock file system responses + mockFs.setFileContent(`${expectedBareDir}/HEAD`, 'ref: refs/heads/main\n'); + mockFs.setDirectory(expectedTargetDir); + mockFs.setDirectory(expectedBareDir); + + // Mock git responses + mockGit.setCommandResponse(['clone', '--bare', 'https://github.com/user/repo.git', expectedBareDir], ''); + mockGit.setCommandResponse(['config', 'remote.origin.fetch', '+refs/heads/*:refs/remotes/origin/*'], ''); + mockGit.setCommandResponse(['fetch', 'origin'], ''); + mockGit.setCommandResponse(['show-ref', '--verify', '--quiet', 'refs/remotes/origin/main'], { stdout: '', stderr: '', exitCode: 0 }); + mockGit.setCommandResponse(['for-each-ref', '--format=%(refname)', 'refs/remotes'], { stdout: 'refs/remotes/origin/main\n', stderr: '', exitCode: 0 }); + mockGit.setCommandResponse(['worktree', 'add', '-b', 'main', `${expectedTargetDir}/main`, 'origin/main'], ''); + mockGit.setCommandResponse(['branch', '--set-upstream-to', 'origin/main'], ''); + + // Test initialization + const result = await initOps.initializeRepository('https://github.com/user/repo.git', 'repo'); + + expect(result.rootDir).toBe(expectedTargetDir); + expect(result.gitDir).toBe(expectedBareDir); + expect(result.type).toBe('bare'); + + // Debug: check all logs + console.log('All logs:', mockLogger.getLogsByLevel('log')); + console.log('All warns:', mockLogger.getLogsByLevel('warn')); + + // Verify default branch detection and worktree creation occurred + expect(mockLogger.hasLog('log', 'Detected default branch: main')).toBe(true); + expect(mockLogger.hasLog('log', 'Created worktree for default branch \'main\' with upstream tracking')).toBe(true); +}); + +test('InitOperations - default branch detection fallback to symbolic-ref', async () => { + const mockLogger = new MockLoggerService(); + const mockGit = new MockGitService(); + const mockFs = new MockFileSystemService(); + const mockCmd = new MockCommandService(); + + const services = createServiceContainer({ + logger: mockLogger, + git: mockGit, + fs: mockFs, + cmd: mockCmd + }); + + const initOps = new InitOperations(services); + + const expectedTargetDir = resolve(process.cwd(), 'repo'); + const expectedBareDir = join(expectedTargetDir, '.bare'); + + // Mock file system responses - HEAD file doesn't contain expected format + mockFs.setFileContent(`${expectedBareDir}/HEAD`, 'invalid content\n'); + mockFs.setDirectory(expectedTargetDir); + mockFs.setDirectory(expectedBareDir); + + // Mock git responses for fallback detection + mockGit.setCommandResponse(['clone', '--bare', 'https://github.com/user/repo.git', expectedBareDir], ''); + mockGit.setCommandResponse(['config', 'remote.origin.fetch', '+refs/heads/*:refs/remotes/origin/*'], ''); + mockGit.setCommandResponse(['fetch', 'origin'], ''); + mockGit.setCommandResponse(['symbolic-ref', 'refs/remotes/origin/HEAD'], { + exitCode: 0, + stdout: 'refs/remotes/origin/develop\n', + stderr: '' + }); + mockGit.setCommandResponse(['show-ref', '--verify', '--quiet', 'refs/remotes/origin/develop'], { stdout: '', stderr: '', exitCode: 0 }); + mockGit.setCommandResponse(['for-each-ref', '--format=%(refname)', 'refs/remotes'], { stdout: 'refs/remotes/origin/develop\n', stderr: '', exitCode: 0 }); + mockGit.setCommandResponse(['worktree', 'add', '-b', 'develop', `${expectedTargetDir}/develop`, 'origin/develop'], ''); + mockGit.setCommandResponse(['branch', '--set-upstream-to', 'origin/develop'], ''); + + const result = await initOps.initializeRepository('https://github.com/user/repo.git', 'repo'); + + expect(result.rootDir).toBe(expectedTargetDir); + expect(mockLogger.hasLog('log', 'Detected default branch: develop')).toBe(true); +}); + +test('InitOperations - default branch detection fallback to common defaults', async () => { + const mockLogger = new MockLoggerService(); + const mockGit = new MockGitService(); + const mockFs = new MockFileSystemService(); + const mockCmd = new MockCommandService(); + + const services = createServiceContainer({ + logger: mockLogger, + git: mockGit, + fs: mockFs, + cmd: mockCmd + }); + + const initOps = new InitOperations(services); + + const expectedTargetDir = resolve(process.cwd(), 'repo'); + const expectedBareDir = join(expectedTargetDir, '.bare'); + + // Mock file system responses - HEAD file reading fails but directories exist + mockFs.setDirectory(expectedTargetDir); + mockFs.setDirectory(expectedBareDir); + + // Mock git responses + mockGit.setCommandResponse(['clone', '--bare', 'https://github.com/user/repo.git', expectedBareDir], ''); + mockGit.setCommandResponse(['config', 'remote.origin.fetch', '+refs/heads/*:refs/remotes/origin/*'], ''); + mockGit.setCommandResponse(['fetch', 'origin'], ''); + mockGit.setCommandResponse(['symbolic-ref', 'refs/remotes/origin/HEAD'], { stdout: '', stderr: '', exitCode: 1 }); + + // Mock fallback attempts - first 'main' fails, then 'master' succeeds + mockGit.setCommandResponse(['show-ref', '--verify', '--quiet', 'refs/remotes/origin/main'], { stdout: '', stderr: '', exitCode: 1 }); + mockGit.setCommandResponse(['show-ref', '--verify', '--quiet', 'refs/remotes/origin/master'], { stdout: '', stderr: '', exitCode: 0 }); + mockGit.setCommandResponse(['for-each-ref', '--format=%(refname)', 'refs/remotes'], { stdout: 'refs/remotes/origin/master\n', stderr: '', exitCode: 0 }); + mockGit.setCommandResponse(['worktree', 'add', '-b', 'master', `${expectedTargetDir}/master`, 'origin/master'], ''); + mockGit.setCommandResponse(['branch', '--set-upstream-to', 'origin/master'], ''); + + const result = await initOps.initializeRepository('https://github.com/user/repo.git', 'repo'); + + expect(result.rootDir).toBe(expectedTargetDir); + expect(mockLogger.hasLog('warn', 'Could not detect default branch from HEAD file, using found branch: master')).toBe(true); + expect(mockLogger.hasLog('log', 'Detected default branch: master')).toBe(true); +}); + +test('InitOperations - default branch detection failure with graceful fallback', async () => { + const mockLogger = new MockLoggerService(); + const mockGit = new MockGitService(); + const mockFs = new MockFileSystemService(); + const mockCmd = new MockCommandService(); + + const services = createServiceContainer({ + logger: mockLogger, + git: mockGit, + fs: mockFs, + cmd: mockCmd + }); + + const initOps = new InitOperations(services); + + const expectedTargetDir = resolve(process.cwd(), 'repo'); + const expectedBareDir = join(expectedTargetDir, '.bare'); + + // Mock file system responses + mockFs.setDirectory(expectedTargetDir); + mockFs.setDirectory(expectedBareDir); + + // Mock git responses - all detection methods fail + mockGit.setCommandResponse(['clone', '--bare', 'https://github.com/user/repo.git', expectedBareDir], ''); + mockGit.setCommandResponse(['config', 'remote.origin.fetch', '+refs/heads/*:refs/remotes/origin/*'], ''); + mockGit.setCommandResponse(['fetch', 'origin'], ''); + mockGit.setCommandResponse(['symbolic-ref', 'refs/remotes/origin/HEAD'], { stdout: '', stderr: '', exitCode: 1 }); + + // All common default branches fail + mockGit.setCommandResponse(['show-ref', '--verify', '--quiet', 'refs/remotes/origin/main'], { stdout: '', stderr: '', exitCode: 1 }); + mockGit.setCommandResponse(['show-ref', '--verify', '--quiet', 'refs/remotes/origin/master'], { stdout: '', stderr: '', exitCode: 1 }); + mockGit.setCommandResponse(['show-ref', '--verify', '--quiet', 'refs/remotes/origin/develop'], { stdout: '', stderr: '', exitCode: 1 }); + mockGit.setCommandResponse(['show-ref', '--verify', '--quiet', 'refs/remotes/origin/development'], { stdout: '', stderr: '', exitCode: 1 }); + + const result = await initOps.initializeRepository('https://github.com/user/repo.git', 'repo'); + + // Should still succeed overall but show warning about worktree creation failure + expect(result.rootDir).toBe(expectedTargetDir); + expect(mockLogger.hasLogContaining('warn', 'Warning: Failed to create default branch worktree:')).toBe(true); +}); + +test('InitOperations - upstream tracking setup failure should warn but continue', async () => { + const mockLogger = new MockLoggerService(); + const mockGit = new MockGitService(); + const mockFs = new MockFileSystemService(); + const mockCmd = new MockCommandService(); + + const services = createServiceContainer({ + logger: mockLogger, + git: mockGit, + fs: mockFs, + cmd: mockCmd + }); + + const initOps = new InitOperations(services); + + const expectedTargetDir = resolve(process.cwd(), 'repo'); + const expectedBareDir = join(expectedTargetDir, '.bare'); + + // Mock file system responses + mockFs.setFileContent(`${expectedBareDir}/HEAD`, 'ref: refs/heads/main\n'); + mockFs.setDirectory(expectedTargetDir); + mockFs.setDirectory(expectedBareDir); + + // Mock git responses - worktree creation succeeds but upstream setup fails + mockGit.setCommandResponse(['clone', '--bare', 'https://github.com/user/repo.git', expectedBareDir], ''); + mockGit.setCommandResponse(['config', 'remote.origin.fetch', '+refs/heads/*:refs/remotes/origin/*'], ''); + mockGit.setCommandResponse(['fetch', 'origin'], ''); + mockGit.setCommandResponse(['show-ref', '--verify', '--quiet', 'refs/remotes/origin/main'], { stdout: '', stderr: '', exitCode: 0 }); + mockGit.setCommandResponse(['for-each-ref', '--format=%(refname)', 'refs/remotes'], { stdout: 'refs/remotes/origin/main\n', stderr: '', exitCode: 0 }); + mockGit.setCommandResponse(['worktree', 'add', '-b', 'main', `${expectedTargetDir}/main`, 'origin/main'], ''); + mockGit.setCommandResponse(['branch', '--set-upstream-to', 'origin/main'], { + exitCode: 1, + stderr: 'upstream setup failed', + stdout: '' + }); + + const result = await initOps.initializeRepository('https://github.com/user/repo.git', 'repo'); + + expect(result.rootDir).toBe(expectedTargetDir); + expect(mockLogger.hasLog('log', 'Detected default branch: main')).toBe(true); + expect(mockLogger.hasLogContaining('warn', 'Warning: Failed to set upstream tracking for branch \'main\':')).toBe(true); }); \ No newline at end of file