mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-03-18 10:23:07 +00:00
Fix concurrency limits and remote branch fetching issues (#788)
* Changes from fix/bug-fixes * feat: Refactor worktree iteration and improve error logging across services * feat: Extract URL/port patterns to module level and fix abort condition * fix: Improve IPv6 loopback handling, select component layout, and terminal UI * feat: Add thinking level defaults and adjust list row padding * Update apps/ui/src/store/app-store.ts Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * feat: Add worktree-aware terminal creation and split options, fix npm security issues from audit * feat: Add tracked remote detection to pull dialog flow * feat: Add merge state tracking to git operations * feat: Improve merge detection and add post-merge action preferences * Update apps/ui/src/components/views/board-view/dialogs/git-pull-dialog.tsx Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * Update apps/ui/src/components/views/board-view/dialogs/git-pull-dialog.tsx Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * fix: Pass merge detection info to stash reapplication and handle merge state consistently * fix: Call onPulled callback in merge handlers and add validation checks --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
This commit is contained in:
@@ -328,6 +328,86 @@ describe('auto-loop-coordinator.ts', () => {
|
||||
// Should not have executed features because at capacity
|
||||
expect(mockExecuteFeature).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('counts all running features (auto + manual) against concurrency limit', async () => {
|
||||
vi.mocked(mockLoadPendingFeatures).mockResolvedValue([testFeature]);
|
||||
// 2 manual features running — total count is 2
|
||||
vi.mocked(mockConcurrencyManager.getRunningCountForWorktree).mockResolvedValue(2);
|
||||
|
||||
await coordinator.startAutoLoopForProject('/test/project', null, 2);
|
||||
|
||||
await vi.advanceTimersByTimeAsync(6000);
|
||||
|
||||
await coordinator.stopAutoLoopForProject('/test/project', null);
|
||||
|
||||
// Should NOT execute because total running count (2) meets the concurrency limit (2)
|
||||
expect(mockExecuteFeature).not.toHaveBeenCalled();
|
||||
// Verify it was called WITHOUT autoModeOnly (counts all tasks)
|
||||
// The coordinator's wrapper passes options through as undefined when not specified
|
||||
expect(mockConcurrencyManager.getRunningCountForWorktree).toHaveBeenCalledWith(
|
||||
'/test/project',
|
||||
null,
|
||||
undefined
|
||||
);
|
||||
});
|
||||
|
||||
it('allows auto dispatch when manual tasks finish and capacity becomes available', async () => {
|
||||
vi.mocked(mockLoadPendingFeatures).mockResolvedValue([testFeature]);
|
||||
// First call: at capacity (2 manual features running)
|
||||
// Second call: capacity freed (1 feature running)
|
||||
vi.mocked(mockConcurrencyManager.getRunningCountForWorktree)
|
||||
.mockResolvedValueOnce(2) // at capacity
|
||||
.mockResolvedValueOnce(1); // capacity available after manual task completes
|
||||
|
||||
await coordinator.startAutoLoopForProject('/test/project', null, 2);
|
||||
|
||||
// First iteration: at capacity, should wait
|
||||
await vi.advanceTimersByTimeAsync(5000);
|
||||
|
||||
// Second iteration: capacity available, should execute
|
||||
await vi.advanceTimersByTimeAsync(6000);
|
||||
|
||||
await coordinator.stopAutoLoopForProject('/test/project', null);
|
||||
|
||||
// Should execute after capacity freed
|
||||
expect(mockExecuteFeature).toHaveBeenCalledWith('/test/project', 'feature-1', true, true);
|
||||
});
|
||||
|
||||
it('waits when manually started tasks already fill concurrency limit at auto mode activation', async () => {
|
||||
vi.mocked(mockLoadPendingFeatures).mockResolvedValue([testFeature]);
|
||||
// Manual tasks already fill the limit
|
||||
vi.mocked(mockConcurrencyManager.getRunningCountForWorktree).mockResolvedValue(3);
|
||||
|
||||
await coordinator.startAutoLoopForProject('/test/project', null, 3);
|
||||
|
||||
await vi.advanceTimersByTimeAsync(6000);
|
||||
|
||||
await coordinator.stopAutoLoopForProject('/test/project', null);
|
||||
|
||||
// Auto mode should remain waiting, not dispatch
|
||||
expect(mockExecuteFeature).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('resumes dispatching when all running tasks complete simultaneously', async () => {
|
||||
vi.mocked(mockLoadPendingFeatures).mockResolvedValue([testFeature]);
|
||||
// First check: all 3 slots occupied
|
||||
// Second check: all tasks completed simultaneously
|
||||
vi.mocked(mockConcurrencyManager.getRunningCountForWorktree)
|
||||
.mockResolvedValueOnce(3) // all slots full
|
||||
.mockResolvedValueOnce(0); // all tasks completed at once
|
||||
|
||||
await coordinator.startAutoLoopForProject('/test/project', null, 3);
|
||||
|
||||
// First iteration: at capacity
|
||||
await vi.advanceTimersByTimeAsync(5000);
|
||||
// Second iteration: all freed
|
||||
await vi.advanceTimersByTimeAsync(6000);
|
||||
|
||||
await coordinator.stopAutoLoopForProject('/test/project', null);
|
||||
|
||||
// Should execute after all tasks freed capacity
|
||||
expect(mockExecuteFeature).toHaveBeenCalledWith('/test/project', 'feature-1', true, true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('priority-based feature selection', () => {
|
||||
@@ -788,7 +868,23 @@ describe('auto-loop-coordinator.ts', () => {
|
||||
expect(count).toBe(3);
|
||||
expect(mockConcurrencyManager.getRunningCountForWorktree).toHaveBeenCalledWith(
|
||||
'/test/project',
|
||||
null
|
||||
null,
|
||||
undefined
|
||||
);
|
||||
});
|
||||
|
||||
it('passes autoModeOnly option to ConcurrencyManager', async () => {
|
||||
vi.mocked(mockConcurrencyManager.getRunningCountForWorktree).mockResolvedValue(1);
|
||||
|
||||
const count = await coordinator.getRunningCountForWorktree('/test/project', null, {
|
||||
autoModeOnly: true,
|
||||
});
|
||||
|
||||
expect(count).toBe(1);
|
||||
expect(mockConcurrencyManager.getRunningCountForWorktree).toHaveBeenCalledWith(
|
||||
'/test/project',
|
||||
null,
|
||||
{ autoModeOnly: true }
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -416,6 +416,90 @@ describe('ConcurrencyManager', () => {
|
||||
expect(mainCount).toBe(2);
|
||||
});
|
||||
|
||||
it('should count only auto-mode features when autoModeOnly is true', async () => {
|
||||
// Auto-mode feature on main worktree
|
||||
manager.acquire({
|
||||
featureId: 'feature-auto',
|
||||
projectPath: '/test/project',
|
||||
isAutoMode: true,
|
||||
});
|
||||
|
||||
// Manual feature on main worktree
|
||||
manager.acquire({
|
||||
featureId: 'feature-manual',
|
||||
projectPath: '/test/project',
|
||||
isAutoMode: false,
|
||||
});
|
||||
|
||||
// Without autoModeOnly: counts both
|
||||
const totalCount = await manager.getRunningCountForWorktree('/test/project', null);
|
||||
expect(totalCount).toBe(2);
|
||||
|
||||
// With autoModeOnly: counts only auto-mode features
|
||||
const autoModeCount = await manager.getRunningCountForWorktree('/test/project', null, {
|
||||
autoModeOnly: true,
|
||||
});
|
||||
expect(autoModeCount).toBe(1);
|
||||
});
|
||||
|
||||
it('should count only auto-mode features on specific worktree when autoModeOnly is true', async () => {
|
||||
// Auto-mode feature on feature branch
|
||||
manager.acquire({
|
||||
featureId: 'feature-auto',
|
||||
projectPath: '/test/project',
|
||||
isAutoMode: true,
|
||||
});
|
||||
manager.updateRunningFeature('feature-auto', { branchName: 'feature-branch' });
|
||||
|
||||
// Manual feature on same feature branch
|
||||
manager.acquire({
|
||||
featureId: 'feature-manual',
|
||||
projectPath: '/test/project',
|
||||
isAutoMode: false,
|
||||
});
|
||||
manager.updateRunningFeature('feature-manual', { branchName: 'feature-branch' });
|
||||
|
||||
// Another auto-mode feature on different branch (should not be counted)
|
||||
manager.acquire({
|
||||
featureId: 'feature-other',
|
||||
projectPath: '/test/project',
|
||||
isAutoMode: true,
|
||||
});
|
||||
manager.updateRunningFeature('feature-other', { branchName: 'other-branch' });
|
||||
|
||||
const autoModeCount = await manager.getRunningCountForWorktree(
|
||||
'/test/project',
|
||||
'feature-branch',
|
||||
{ autoModeOnly: true }
|
||||
);
|
||||
expect(autoModeCount).toBe(1);
|
||||
|
||||
const totalCount = await manager.getRunningCountForWorktree(
|
||||
'/test/project',
|
||||
'feature-branch'
|
||||
);
|
||||
expect(totalCount).toBe(2);
|
||||
});
|
||||
|
||||
it('should return 0 when autoModeOnly is true and only manual features are running', async () => {
|
||||
manager.acquire({
|
||||
featureId: 'feature-manual-1',
|
||||
projectPath: '/test/project',
|
||||
isAutoMode: false,
|
||||
});
|
||||
|
||||
manager.acquire({
|
||||
featureId: 'feature-manual-2',
|
||||
projectPath: '/test/project',
|
||||
isAutoMode: false,
|
||||
});
|
||||
|
||||
const autoModeCount = await manager.getRunningCountForWorktree('/test/project', null, {
|
||||
autoModeOnly: true,
|
||||
});
|
||||
expect(autoModeCount).toBe(0);
|
||||
});
|
||||
|
||||
it('should filter by both projectPath and branchName', async () => {
|
||||
manager.acquire({
|
||||
featureId: 'feature-1',
|
||||
|
||||
@@ -486,7 +486,7 @@ describe('dev-server-service.ts', () => {
|
||||
await service.startDevServer(testDir, testDir);
|
||||
|
||||
// Simulate HTTPS dev server
|
||||
mockProcess.stdout.emit('data', Buffer.from('Server at https://localhost:3443\n'));
|
||||
mockProcess.stdout.emit('data', Buffer.from('Server listening at https://localhost:3443\n'));
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
|
||||
@@ -521,6 +521,368 @@ describe('dev-server-service.ts', () => {
|
||||
expect(serverInfo?.url).toBe(firstUrl);
|
||||
expect(serverInfo?.url).toBe('http://localhost:5173/');
|
||||
});
|
||||
|
||||
it('should detect Astro format URL', async () => {
|
||||
vi.mocked(secureFs.access).mockResolvedValue(undefined);
|
||||
|
||||
const mockProcess = createMockProcess();
|
||||
vi.mocked(spawn).mockReturnValue(mockProcess as any);
|
||||
|
||||
const { getDevServerService } = await import('@/services/dev-server-service.js');
|
||||
const service = getDevServerService();
|
||||
|
||||
await service.startDevServer(testDir, testDir);
|
||||
|
||||
// Astro uses the same "Local:" prefix as Vite
|
||||
mockProcess.stdout.emit('data', Buffer.from(' 🚀 astro v4.0.0 started in 200ms\n'));
|
||||
mockProcess.stdout.emit('data', Buffer.from(' ┃ Local http://localhost:4321/\n'));
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
|
||||
const serverInfo = service.getServerInfo(testDir);
|
||||
// Astro doesn't use "Local:" with colon, so it should be caught by the localhost URL pattern
|
||||
expect(serverInfo?.url).toBe('http://localhost:4321/');
|
||||
expect(serverInfo?.urlDetected).toBe(true);
|
||||
});
|
||||
|
||||
it('should detect Remix format URL', async () => {
|
||||
vi.mocked(secureFs.access).mockResolvedValue(undefined);
|
||||
|
||||
const mockProcess = createMockProcess();
|
||||
vi.mocked(spawn).mockReturnValue(mockProcess as any);
|
||||
|
||||
const { getDevServerService } = await import('@/services/dev-server-service.js');
|
||||
const service = getDevServerService();
|
||||
|
||||
await service.startDevServer(testDir, testDir);
|
||||
|
||||
mockProcess.stdout.emit(
|
||||
'data',
|
||||
Buffer.from('Remix App Server started at http://localhost:3000\n')
|
||||
);
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
|
||||
const serverInfo = service.getServerInfo(testDir);
|
||||
expect(serverInfo?.url).toBe('http://localhost:3000');
|
||||
expect(serverInfo?.urlDetected).toBe(true);
|
||||
});
|
||||
|
||||
it('should detect Django format URL', async () => {
|
||||
vi.mocked(secureFs.access).mockResolvedValue(undefined);
|
||||
|
||||
const mockProcess = createMockProcess();
|
||||
vi.mocked(spawn).mockReturnValue(mockProcess as any);
|
||||
|
||||
const { getDevServerService } = await import('@/services/dev-server-service.js');
|
||||
const service = getDevServerService();
|
||||
|
||||
await service.startDevServer(testDir, testDir);
|
||||
|
||||
mockProcess.stdout.emit(
|
||||
'data',
|
||||
Buffer.from('Starting development server at http://127.0.0.1:8000/\n')
|
||||
);
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
|
||||
const serverInfo = service.getServerInfo(testDir);
|
||||
expect(serverInfo?.url).toBe('http://127.0.0.1:8000/');
|
||||
expect(serverInfo?.urlDetected).toBe(true);
|
||||
});
|
||||
|
||||
it('should detect Webpack Dev Server format URL', async () => {
|
||||
vi.mocked(secureFs.access).mockResolvedValue(undefined);
|
||||
|
||||
const mockProcess = createMockProcess();
|
||||
vi.mocked(spawn).mockReturnValue(mockProcess as any);
|
||||
|
||||
const { getDevServerService } = await import('@/services/dev-server-service.js');
|
||||
const service = getDevServerService();
|
||||
|
||||
await service.startDevServer(testDir, testDir);
|
||||
|
||||
mockProcess.stdout.emit(
|
||||
'data',
|
||||
Buffer.from('<i> [webpack-dev-server] Project is running at http://localhost:8080/\n')
|
||||
);
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
|
||||
const serverInfo = service.getServerInfo(testDir);
|
||||
expect(serverInfo?.url).toBe('http://localhost:8080/');
|
||||
expect(serverInfo?.urlDetected).toBe(true);
|
||||
});
|
||||
|
||||
it('should detect PHP built-in server format URL', async () => {
|
||||
vi.mocked(secureFs.access).mockResolvedValue(undefined);
|
||||
|
||||
const mockProcess = createMockProcess();
|
||||
vi.mocked(spawn).mockReturnValue(mockProcess as any);
|
||||
|
||||
const { getDevServerService } = await import('@/services/dev-server-service.js');
|
||||
const service = getDevServerService();
|
||||
|
||||
await service.startDevServer(testDir, testDir);
|
||||
|
||||
mockProcess.stdout.emit(
|
||||
'data',
|
||||
Buffer.from('Development Server (http://localhost:8000) started\n')
|
||||
);
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
|
||||
const serverInfo = service.getServerInfo(testDir);
|
||||
expect(serverInfo?.url).toBe('http://localhost:8000');
|
||||
expect(serverInfo?.urlDetected).toBe(true);
|
||||
});
|
||||
|
||||
it('should detect "listening on port" format (port-only detection)', async () => {
|
||||
vi.mocked(secureFs.access).mockResolvedValue(undefined);
|
||||
|
||||
const mockProcess = createMockProcess();
|
||||
vi.mocked(spawn).mockReturnValue(mockProcess as any);
|
||||
|
||||
const { getDevServerService } = await import('@/services/dev-server-service.js');
|
||||
const service = getDevServerService();
|
||||
|
||||
await service.startDevServer(testDir, testDir);
|
||||
|
||||
// Some servers only print the port number, not a full URL
|
||||
mockProcess.stdout.emit('data', Buffer.from('Server listening on port 4000\n'));
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
|
||||
const serverInfo = service.getServerInfo(testDir);
|
||||
expect(serverInfo?.url).toBe('http://localhost:4000');
|
||||
expect(serverInfo?.urlDetected).toBe(true);
|
||||
});
|
||||
|
||||
it('should detect "running on port" format (port-only detection)', async () => {
|
||||
vi.mocked(secureFs.access).mockResolvedValue(undefined);
|
||||
|
||||
const mockProcess = createMockProcess();
|
||||
vi.mocked(spawn).mockReturnValue(mockProcess as any);
|
||||
|
||||
const { getDevServerService } = await import('@/services/dev-server-service.js');
|
||||
const service = getDevServerService();
|
||||
|
||||
await service.startDevServer(testDir, testDir);
|
||||
|
||||
mockProcess.stdout.emit('data', Buffer.from('Application running on port 9000\n'));
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
|
||||
const serverInfo = service.getServerInfo(testDir);
|
||||
expect(serverInfo?.url).toBe('http://localhost:9000');
|
||||
expect(serverInfo?.urlDetected).toBe(true);
|
||||
});
|
||||
|
||||
it('should strip ANSI escape codes before detecting URL', async () => {
|
||||
vi.mocked(secureFs.access).mockResolvedValue(undefined);
|
||||
|
||||
const mockProcess = createMockProcess();
|
||||
vi.mocked(spawn).mockReturnValue(mockProcess as any);
|
||||
|
||||
const { getDevServerService } = await import('@/services/dev-server-service.js');
|
||||
const service = getDevServerService();
|
||||
|
||||
await service.startDevServer(testDir, testDir);
|
||||
|
||||
// Simulate Vite output with ANSI color codes
|
||||
mockProcess.stdout.emit(
|
||||
'data',
|
||||
Buffer.from(
|
||||
' \x1B[32m➜\x1B[0m \x1B[1mLocal:\x1B[0m \x1B[36mhttp://localhost:5173/\x1B[0m\n'
|
||||
)
|
||||
);
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
|
||||
const serverInfo = service.getServerInfo(testDir);
|
||||
expect(serverInfo?.url).toBe('http://localhost:5173/');
|
||||
expect(serverInfo?.urlDetected).toBe(true);
|
||||
});
|
||||
|
||||
it('should normalize 0.0.0.0 to localhost', async () => {
|
||||
vi.mocked(secureFs.access).mockResolvedValue(undefined);
|
||||
|
||||
const mockProcess = createMockProcess();
|
||||
vi.mocked(spawn).mockReturnValue(mockProcess as any);
|
||||
|
||||
const { getDevServerService } = await import('@/services/dev-server-service.js');
|
||||
const service = getDevServerService();
|
||||
|
||||
await service.startDevServer(testDir, testDir);
|
||||
|
||||
mockProcess.stdout.emit('data', Buffer.from('Server listening at http://0.0.0.0:3000\n'));
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
|
||||
const serverInfo = service.getServerInfo(testDir);
|
||||
expect(serverInfo?.url).toBe('http://localhost:3000');
|
||||
expect(serverInfo?.urlDetected).toBe(true);
|
||||
});
|
||||
|
||||
it('should normalize [::] to localhost', async () => {
|
||||
vi.mocked(secureFs.access).mockResolvedValue(undefined);
|
||||
|
||||
const mockProcess = createMockProcess();
|
||||
vi.mocked(spawn).mockReturnValue(mockProcess as any);
|
||||
|
||||
const { getDevServerService } = await import('@/services/dev-server-service.js');
|
||||
const service = getDevServerService();
|
||||
|
||||
await service.startDevServer(testDir, testDir);
|
||||
|
||||
mockProcess.stdout.emit('data', Buffer.from('Local: http://[::]:4000/\n'));
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
|
||||
const serverInfo = service.getServerInfo(testDir);
|
||||
expect(serverInfo?.url).toBe('http://localhost:4000/');
|
||||
expect(serverInfo?.urlDetected).toBe(true);
|
||||
});
|
||||
|
||||
it('should update port field when detected URL has different port', async () => {
|
||||
vi.mocked(secureFs.access).mockResolvedValue(undefined);
|
||||
|
||||
const mockProcess = createMockProcess();
|
||||
vi.mocked(spawn).mockReturnValue(mockProcess as any);
|
||||
|
||||
const { getDevServerService } = await import('@/services/dev-server-service.js');
|
||||
const service = getDevServerService();
|
||||
|
||||
const result = await service.startDevServer(testDir, testDir);
|
||||
const allocatedPort = result.result?.port;
|
||||
|
||||
// Server starts on a completely different port (ignoring PORT env var)
|
||||
mockProcess.stdout.emit('data', Buffer.from('Local: http://localhost:9999/\n'));
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
|
||||
const serverInfo = service.getServerInfo(testDir);
|
||||
expect(serverInfo?.url).toBe('http://localhost:9999/');
|
||||
expect(serverInfo?.port).toBe(9999);
|
||||
// The port should be different from what was initially allocated
|
||||
if (allocatedPort !== 9999) {
|
||||
expect(serverInfo?.port).not.toBe(allocatedPort);
|
||||
}
|
||||
});
|
||||
|
||||
it('should detect URL from stderr output', async () => {
|
||||
vi.mocked(secureFs.access).mockResolvedValue(undefined);
|
||||
|
||||
const mockProcess = createMockProcess();
|
||||
vi.mocked(spawn).mockReturnValue(mockProcess as any);
|
||||
|
||||
const { getDevServerService } = await import('@/services/dev-server-service.js');
|
||||
const service = getDevServerService();
|
||||
|
||||
await service.startDevServer(testDir, testDir);
|
||||
|
||||
// Some servers output URL info to stderr
|
||||
mockProcess.stderr.emit('data', Buffer.from('Local: http://localhost:3000/\n'));
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
|
||||
const serverInfo = service.getServerInfo(testDir);
|
||||
expect(serverInfo?.url).toBe('http://localhost:3000/');
|
||||
expect(serverInfo?.urlDetected).toBe(true);
|
||||
});
|
||||
|
||||
it('should not match URLs without a port (non-dev-server URLs)', async () => {
|
||||
vi.mocked(secureFs.access).mockResolvedValue(undefined);
|
||||
|
||||
const mockProcess = createMockProcess();
|
||||
vi.mocked(spawn).mockReturnValue(mockProcess as any);
|
||||
|
||||
const { getDevServerService } = await import('@/services/dev-server-service.js');
|
||||
const service = getDevServerService();
|
||||
|
||||
const result = await service.startDevServer(testDir, testDir);
|
||||
|
||||
// CDN/external URLs should not be detected
|
||||
mockProcess.stdout.emit(
|
||||
'data',
|
||||
Buffer.from('Downloading from https://cdn.example.com/bundle.js\n')
|
||||
);
|
||||
mockProcess.stdout.emit('data', Buffer.from('Fetching https://registry.npmjs.org/package\n'));
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
|
||||
const serverInfo = service.getServerInfo(testDir);
|
||||
// Should keep the initial allocated URL since external URLs don't match
|
||||
expect(serverInfo?.url).toBe(result.result?.url);
|
||||
expect(serverInfo?.urlDetected).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle URLs with trailing punctuation', async () => {
|
||||
vi.mocked(secureFs.access).mockResolvedValue(undefined);
|
||||
|
||||
const mockProcess = createMockProcess();
|
||||
vi.mocked(spawn).mockReturnValue(mockProcess as any);
|
||||
|
||||
const { getDevServerService } = await import('@/services/dev-server-service.js');
|
||||
const service = getDevServerService();
|
||||
|
||||
await service.startDevServer(testDir, testDir);
|
||||
|
||||
// URL followed by punctuation
|
||||
mockProcess.stdout.emit('data', Buffer.from('Server started at http://localhost:3000.\n'));
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
|
||||
const serverInfo = service.getServerInfo(testDir);
|
||||
expect(serverInfo?.url).toBe('http://localhost:3000');
|
||||
expect(serverInfo?.urlDetected).toBe(true);
|
||||
});
|
||||
|
||||
it('should detect Express/Fastify format URL', async () => {
|
||||
vi.mocked(secureFs.access).mockResolvedValue(undefined);
|
||||
|
||||
const mockProcess = createMockProcess();
|
||||
vi.mocked(spawn).mockReturnValue(mockProcess as any);
|
||||
|
||||
const { getDevServerService } = await import('@/services/dev-server-service.js');
|
||||
const service = getDevServerService();
|
||||
|
||||
await service.startDevServer(testDir, testDir);
|
||||
|
||||
mockProcess.stdout.emit('data', Buffer.from('Server listening on http://localhost:3000\n'));
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
|
||||
const serverInfo = service.getServerInfo(testDir);
|
||||
expect(serverInfo?.url).toBe('http://localhost:3000');
|
||||
expect(serverInfo?.urlDetected).toBe(true);
|
||||
});
|
||||
|
||||
it('should detect Angular CLI format URL', async () => {
|
||||
vi.mocked(secureFs.access).mockResolvedValue(undefined);
|
||||
|
||||
const mockProcess = createMockProcess();
|
||||
vi.mocked(spawn).mockReturnValue(mockProcess as any);
|
||||
|
||||
const { getDevServerService } = await import('@/services/dev-server-service.js');
|
||||
const service = getDevServerService();
|
||||
|
||||
await service.startDevServer(testDir, testDir);
|
||||
|
||||
// Angular CLI output
|
||||
mockProcess.stderr.emit(
|
||||
'data',
|
||||
Buffer.from(
|
||||
'** Angular Live Development Server is listening on localhost:4200, open your browser on http://localhost:4200/ **\n'
|
||||
)
|
||||
);
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
|
||||
const serverInfo = service.getServerInfo(testDir);
|
||||
expect(serverInfo?.url).toBe('http://localhost:4200/');
|
||||
expect(serverInfo?.urlDetected).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -531,6 +893,7 @@ function createMockProcess() {
|
||||
mockProcess.stderr = new EventEmitter();
|
||||
mockProcess.kill = vi.fn();
|
||||
mockProcess.killed = false;
|
||||
mockProcess.pid = 12345;
|
||||
|
||||
// Don't exit immediately - let the test control the lifecycle
|
||||
return mockProcess;
|
||||
|
||||
Reference in New Issue
Block a user