Fix: Dev server detection bug fixes. Settings sync bug fixes. Cli provider fixes. Terminal background/foreground colors (#791)

* Changes from fix/dev-server-state-bug

* feat: Add configurable max turns setting with user overrides. Address pr comments

* fix: Update default behaviors and improve state management across server and UI

* feat: Extract branch sync logic to separate service. Fix settings sync bug. Address pr comments

* refactor: Extract magic numbers to named constants and improve branch tracking logic

- Add DEFAULT_MAX_TURNS (1000) and MAX_ALLOWED_TURNS (2000) constants to settings-helpers
- Replace hardcoded 1000 values with DEFAULT_MAX_TURNS constant throughout codebase
- Improve max turns validation with explicit Number.isFinite check
- Update getTrackingBranch to split on first slash instead of last for better remote parsing
- Change isBranchCheckedOut return type from boolean to string|null to return worktree path
- Add comments explaining skipFetch parameter in worktree creation
- Fix cleanup order in AgentExecutor finally block to run before logging
```

* feat: Add comment refresh and improve model sync in PR dialog
This commit is contained in:
gsxdsm
2026-02-21 08:57:04 -08:00
committed by GitHub
parent c81ea768a7
commit 3ddf26f666
41 changed files with 2705 additions and 274 deletions

View File

@@ -198,7 +198,7 @@ describe('claude-provider.ts', () => {
expect(typeof callArgs.prompt).not.toBe('string');
});
it('should use maxTurns default of 100', async () => {
it('should use maxTurns default of 1000', async () => {
vi.mocked(sdk.query).mockReturnValue(
(async function* () {
yield { type: 'text', text: 'test' };
@@ -216,7 +216,7 @@ describe('claude-provider.ts', () => {
expect(sdk.query).toHaveBeenCalledWith({
prompt: 'Test',
options: expect.objectContaining({
maxTurns: 100,
maxTurns: 1000,
}),
});
});

View File

@@ -0,0 +1,580 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { EventHookService } from '../../../src/services/event-hook-service.js';
import type { EventEmitter, EventCallback, EventType } from '../../../src/lib/events.js';
import type { SettingsService } from '../../../src/services/settings-service.js';
import type { EventHistoryService } from '../../../src/services/event-history-service.js';
import type { FeatureLoader } from '../../../src/services/feature-loader.js';
/**
* Create a mock EventEmitter for testing
*/
function createMockEventEmitter(): EventEmitter & {
subscribers: Set<EventCallback>;
simulateEvent: (type: EventType, payload: unknown) => void;
} {
const subscribers = new Set<EventCallback>();
return {
subscribers,
emit(type: EventType, payload: unknown) {
for (const callback of subscribers) {
callback(type, payload);
}
},
subscribe(callback: EventCallback) {
subscribers.add(callback);
return () => {
subscribers.delete(callback);
};
},
simulateEvent(type: EventType, payload: unknown) {
for (const callback of subscribers) {
callback(type, payload);
}
},
};
}
/**
* Create a mock SettingsService
*/
function createMockSettingsService(hooks: unknown[] = []): SettingsService {
return {
getGlobalSettings: vi.fn().mockResolvedValue({ eventHooks: hooks }),
} as unknown as SettingsService;
}
/**
* Create a mock EventHistoryService
*/
function createMockEventHistoryService() {
return {
storeEvent: vi.fn().mockResolvedValue({ id: 'test-event-id' }),
} as unknown as EventHistoryService;
}
/**
* Create a mock FeatureLoader
*/
function createMockFeatureLoader(features: Record<string, { title: string }> = {}) {
return {
get: vi.fn().mockImplementation((_projectPath: string, featureId: string) => {
return Promise.resolve(features[featureId] || null);
}),
} as unknown as FeatureLoader;
}
describe('EventHookService', () => {
let service: EventHookService;
let mockEmitter: ReturnType<typeof createMockEventEmitter>;
let mockSettingsService: ReturnType<typeof createMockSettingsService>;
let mockEventHistoryService: ReturnType<typeof createMockEventHistoryService>;
let mockFeatureLoader: ReturnType<typeof createMockFeatureLoader>;
beforeEach(() => {
service = new EventHookService();
mockEmitter = createMockEventEmitter();
mockSettingsService = createMockSettingsService();
mockEventHistoryService = createMockEventHistoryService();
mockFeatureLoader = createMockFeatureLoader();
});
afterEach(() => {
service.destroy();
});
describe('initialize', () => {
it('should subscribe to the event emitter', () => {
service.initialize(mockEmitter, mockSettingsService, mockEventHistoryService);
expect(mockEmitter.subscribers.size).toBe(1);
});
it('should log initialization', () => {
service.initialize(mockEmitter, mockSettingsService);
expect(mockEmitter.subscribers.size).toBe(1);
});
});
describe('destroy', () => {
it('should unsubscribe from the event emitter', () => {
service.initialize(mockEmitter, mockSettingsService);
expect(mockEmitter.subscribers.size).toBe(1);
service.destroy();
expect(mockEmitter.subscribers.size).toBe(0);
});
});
describe('event mapping - auto_mode_feature_complete', () => {
it('should map to feature_success when passes is true', async () => {
service.initialize(
mockEmitter,
mockSettingsService,
mockEventHistoryService,
mockFeatureLoader
);
mockEmitter.simulateEvent('auto-mode:event', {
type: 'auto_mode_feature_complete',
featureId: 'feat-1',
featureName: 'Test Feature',
passes: true,
message: 'Feature completed in 30s',
projectPath: '/test/project',
});
// Allow async processing
await vi.waitFor(() => {
expect(mockEventHistoryService.storeEvent).toHaveBeenCalled();
});
const storeCall = (mockEventHistoryService.storeEvent as ReturnType<typeof vi.fn>).mock
.calls[0][0];
expect(storeCall.trigger).toBe('feature_success');
expect(storeCall.passes).toBe(true);
});
it('should map to feature_error when passes is false', async () => {
service.initialize(
mockEmitter,
mockSettingsService,
mockEventHistoryService,
mockFeatureLoader
);
mockEmitter.simulateEvent('auto-mode:event', {
type: 'auto_mode_feature_complete',
featureId: 'feat-1',
featureName: 'Test Feature',
passes: false,
message: 'Feature stopped by user',
projectPath: '/test/project',
});
await vi.waitFor(() => {
expect(mockEventHistoryService.storeEvent).toHaveBeenCalled();
});
const storeCall = (mockEventHistoryService.storeEvent as ReturnType<typeof vi.fn>).mock
.calls[0][0];
expect(storeCall.trigger).toBe('feature_error');
expect(storeCall.passes).toBe(false);
});
it('should NOT populate error field for successful feature completion', async () => {
service.initialize(
mockEmitter,
mockSettingsService,
mockEventHistoryService,
mockFeatureLoader
);
mockEmitter.simulateEvent('auto-mode:event', {
type: 'auto_mode_feature_complete',
featureId: 'feat-1',
featureName: 'Test Feature',
passes: true,
message: 'Feature completed in 30s - auto-verified',
projectPath: '/test/project',
});
await vi.waitFor(() => {
expect(mockEventHistoryService.storeEvent).toHaveBeenCalled();
});
const storeCall = (mockEventHistoryService.storeEvent as ReturnType<typeof vi.fn>).mock
.calls[0][0];
expect(storeCall.trigger).toBe('feature_success');
// Critical: error should NOT contain the success message
expect(storeCall.error).toBeUndefined();
expect(storeCall.errorType).toBeUndefined();
});
it('should populate error field for failed feature completion', async () => {
service.initialize(
mockEmitter,
mockSettingsService,
mockEventHistoryService,
mockFeatureLoader
);
mockEmitter.simulateEvent('auto-mode:event', {
type: 'auto_mode_feature_complete',
featureId: 'feat-1',
featureName: 'Test Feature',
passes: false,
message: 'Feature stopped by user',
projectPath: '/test/project',
});
await vi.waitFor(() => {
expect(mockEventHistoryService.storeEvent).toHaveBeenCalled();
});
const storeCall = (mockEventHistoryService.storeEvent as ReturnType<typeof vi.fn>).mock
.calls[0][0];
expect(storeCall.trigger).toBe('feature_error');
// Error field should be populated for error triggers
expect(storeCall.error).toBe('Feature stopped by user');
});
});
describe('event mapping - auto_mode_error', () => {
it('should map to feature_error when featureId is present', async () => {
service.initialize(
mockEmitter,
mockSettingsService,
mockEventHistoryService,
mockFeatureLoader
);
mockEmitter.simulateEvent('auto-mode:event', {
type: 'auto_mode_error',
featureId: 'feat-1',
error: 'Network timeout',
errorType: 'network',
projectPath: '/test/project',
});
await vi.waitFor(() => {
expect(mockEventHistoryService.storeEvent).toHaveBeenCalled();
});
const storeCall = (mockEventHistoryService.storeEvent as ReturnType<typeof vi.fn>).mock
.calls[0][0];
expect(storeCall.trigger).toBe('feature_error');
expect(storeCall.error).toBe('Network timeout');
expect(storeCall.errorType).toBe('network');
});
it('should map to auto_mode_error when featureId is not present', async () => {
service.initialize(
mockEmitter,
mockSettingsService,
mockEventHistoryService,
mockFeatureLoader
);
mockEmitter.simulateEvent('auto-mode:event', {
type: 'auto_mode_error',
error: 'System error',
errorType: 'system',
projectPath: '/test/project',
});
await vi.waitFor(() => {
expect(mockEventHistoryService.storeEvent).toHaveBeenCalled();
});
const storeCall = (mockEventHistoryService.storeEvent as ReturnType<typeof vi.fn>).mock
.calls[0][0];
expect(storeCall.trigger).toBe('auto_mode_error');
expect(storeCall.error).toBe('System error');
expect(storeCall.errorType).toBe('system');
});
});
describe('event mapping - auto_mode_idle', () => {
it('should map to auto_mode_complete', async () => {
service.initialize(
mockEmitter,
mockSettingsService,
mockEventHistoryService,
mockFeatureLoader
);
mockEmitter.simulateEvent('auto-mode:event', {
type: 'auto_mode_idle',
projectPath: '/test/project',
});
await vi.waitFor(() => {
expect(mockEventHistoryService.storeEvent).toHaveBeenCalled();
});
const storeCall = (mockEventHistoryService.storeEvent as ReturnType<typeof vi.fn>).mock
.calls[0][0];
expect(storeCall.trigger).toBe('auto_mode_complete');
});
});
describe('event mapping - feature:created', () => {
it('should trigger feature_created hook', async () => {
service.initialize(
mockEmitter,
mockSettingsService,
mockEventHistoryService,
mockFeatureLoader
);
mockEmitter.simulateEvent('feature:created', {
featureId: 'feat-1',
featureName: 'New Feature',
projectPath: '/test/project',
});
await vi.waitFor(() => {
expect(mockEventHistoryService.storeEvent).toHaveBeenCalled();
});
const storeCall = (mockEventHistoryService.storeEvent as ReturnType<typeof vi.fn>).mock
.calls[0][0];
expect(storeCall.trigger).toBe('feature_created');
expect(storeCall.featureId).toBe('feat-1');
});
});
describe('event mapping - unhandled events', () => {
it('should ignore auto-mode events with unrecognized types', async () => {
service.initialize(
mockEmitter,
mockSettingsService,
mockEventHistoryService,
mockFeatureLoader
);
mockEmitter.simulateEvent('auto-mode:event', {
type: 'auto_mode_progress',
featureId: 'feat-1',
content: 'Working...',
projectPath: '/test/project',
});
// Give it time to process
await new Promise((resolve) => setTimeout(resolve, 50));
expect(mockEventHistoryService.storeEvent).not.toHaveBeenCalled();
});
it('should ignore events without a type', async () => {
service.initialize(
mockEmitter,
mockSettingsService,
mockEventHistoryService,
mockFeatureLoader
);
mockEmitter.simulateEvent('auto-mode:event', {
featureId: 'feat-1',
projectPath: '/test/project',
});
await new Promise((resolve) => setTimeout(resolve, 50));
expect(mockEventHistoryService.storeEvent).not.toHaveBeenCalled();
});
});
describe('hook execution', () => {
it('should execute matching enabled hooks for feature_success', async () => {
const hooks = [
{
id: 'hook-1',
enabled: true,
trigger: 'feature_success',
name: 'Success Hook',
action: {
type: 'shell',
command: 'echo "success"',
},
},
{
id: 'hook-2',
enabled: true,
trigger: 'feature_error',
name: 'Error Hook',
action: {
type: 'shell',
command: 'echo "error"',
},
},
];
mockSettingsService = createMockSettingsService(hooks);
service.initialize(
mockEmitter,
mockSettingsService,
mockEventHistoryService,
mockFeatureLoader
);
mockEmitter.simulateEvent('auto-mode:event', {
type: 'auto_mode_feature_complete',
featureId: 'feat-1',
featureName: 'Test Feature',
passes: true,
message: 'Feature completed in 30s',
projectPath: '/test/project',
});
await vi.waitFor(() => {
expect(mockSettingsService.getGlobalSettings).toHaveBeenCalled();
});
// The error hook should NOT have been triggered for a success event
const storeCall = (mockEventHistoryService.storeEvent as ReturnType<typeof vi.fn>).mock
.calls[0][0];
expect(storeCall.trigger).toBe('feature_success');
});
it('should NOT execute error hooks when feature completes successfully', async () => {
// This is the key regression test for the bug:
// "Error event hook fired when a feature completes successfully"
const errorHookCommand = vi.fn();
const hooks = [
{
id: 'hook-error',
enabled: true,
trigger: 'feature_error',
name: 'Error Notification',
action: {
type: 'shell',
command: 'echo "ERROR FIRED"',
},
},
];
mockSettingsService = createMockSettingsService(hooks);
service.initialize(
mockEmitter,
mockSettingsService,
mockEventHistoryService,
mockFeatureLoader
);
mockEmitter.simulateEvent('auto-mode:event', {
type: 'auto_mode_feature_complete',
featureId: 'feat-1',
featureName: 'Test Feature',
passes: true,
message: 'Feature completed in 30s - auto-verified',
projectPath: '/test/project',
});
await vi.waitFor(() => {
expect(mockEventHistoryService.storeEvent).toHaveBeenCalled();
});
// Verify the trigger was feature_success, not feature_error
const storeCall = (mockEventHistoryService.storeEvent as ReturnType<typeof vi.fn>).mock
.calls[0][0];
expect(storeCall.trigger).toBe('feature_success');
// And no error information should be present
expect(storeCall.error).toBeUndefined();
expect(storeCall.errorType).toBeUndefined();
});
});
describe('feature name loading', () => {
it('should load feature name from feature loader when not in payload', async () => {
mockFeatureLoader = createMockFeatureLoader({
'feat-1': { title: 'Loaded Feature Title' },
});
service.initialize(
mockEmitter,
mockSettingsService,
mockEventHistoryService,
mockFeatureLoader
);
mockEmitter.simulateEvent('auto-mode:event', {
type: 'auto_mode_feature_complete',
featureId: 'feat-1',
passes: true,
message: 'Done',
projectPath: '/test/project',
});
await vi.waitFor(() => {
expect(mockEventHistoryService.storeEvent).toHaveBeenCalled();
});
const storeCall = (mockEventHistoryService.storeEvent as ReturnType<typeof vi.fn>).mock
.calls[0][0];
expect(storeCall.featureName).toBe('Loaded Feature Title');
});
it('should fall back to payload featureName when loader fails', async () => {
mockFeatureLoader = createMockFeatureLoader({}); // Empty - no features found
service.initialize(
mockEmitter,
mockSettingsService,
mockEventHistoryService,
mockFeatureLoader
);
mockEmitter.simulateEvent('auto-mode:event', {
type: 'auto_mode_feature_complete',
featureId: 'feat-1',
featureName: 'Fallback Name',
passes: true,
message: 'Done',
projectPath: '/test/project',
});
await vi.waitFor(() => {
expect(mockEventHistoryService.storeEvent).toHaveBeenCalled();
});
const storeCall = (mockEventHistoryService.storeEvent as ReturnType<typeof vi.fn>).mock
.calls[0][0];
expect(storeCall.featureName).toBe('Fallback Name');
});
});
describe('error context for error events', () => {
it('should use payload.error when available for error triggers', async () => {
service.initialize(
mockEmitter,
mockSettingsService,
mockEventHistoryService,
mockFeatureLoader
);
mockEmitter.simulateEvent('auto-mode:event', {
type: 'auto_mode_error',
featureId: 'feat-1',
error: 'Authentication failed',
errorType: 'auth',
projectPath: '/test/project',
});
await vi.waitFor(() => {
expect(mockEventHistoryService.storeEvent).toHaveBeenCalled();
});
const storeCall = (mockEventHistoryService.storeEvent as ReturnType<typeof vi.fn>).mock
.calls[0][0];
expect(storeCall.error).toBe('Authentication failed');
expect(storeCall.errorType).toBe('auth');
});
it('should fall back to payload.message for error field in error triggers', async () => {
service.initialize(
mockEmitter,
mockSettingsService,
mockEventHistoryService,
mockFeatureLoader
);
mockEmitter.simulateEvent('auto-mode:event', {
type: 'auto_mode_feature_complete',
featureId: 'feat-1',
passes: false,
message: 'Feature stopped by user',
projectPath: '/test/project',
});
await vi.waitFor(() => {
expect(mockEventHistoryService.storeEvent).toHaveBeenCalled();
});
const storeCall = (mockEventHistoryService.storeEvent as ReturnType<typeof vi.fn>).mock
.calls[0][0];
expect(storeCall.trigger).toBe('feature_error');
expect(storeCall.error).toBe('Feature stopped by user');
});
});
});

View File

@@ -175,7 +175,10 @@ describe('execution-service.ts', () => {
} as unknown as TypedEventBus;
mockConcurrencyManager = {
acquire: vi.fn().mockImplementation(({ featureId }) => createRunningFeature(featureId)),
acquire: vi.fn().mockImplementation(({ featureId, isAutoMode }) => ({
...createRunningFeature(featureId),
isAutoMode: isAutoMode ?? false,
})),
release: vi.fn(),
getRunningFeature: vi.fn(),
isRunning: vi.fn(),
@@ -550,8 +553,8 @@ describe('execution-service.ts', () => {
expect(mockRunAgentFn).not.toHaveBeenCalled();
});
it('emits feature_complete event on success', async () => {
await service.executeFeature('/test/project', 'feature-1');
it('emits feature_complete event on success when isAutoMode is true', async () => {
await service.executeFeature('/test/project', 'feature-1', false, true);
expect(mockEventBus.emitAutoModeEvent).toHaveBeenCalledWith(
'auto_mode_feature_complete',
@@ -561,6 +564,15 @@ describe('execution-service.ts', () => {
})
);
});
it('does not emit feature_complete event on success when isAutoMode is false', async () => {
await service.executeFeature('/test/project', 'feature-1', false, false);
const completeCalls = vi
.mocked(mockEventBus.emitAutoModeEvent)
.mock.calls.filter((call) => call[0] === 'auto_mode_feature_complete');
expect(completeCalls.length).toBe(0);
});
});
describe('executeFeature - approved plan handling', () => {
@@ -1110,7 +1122,7 @@ describe('execution-service.ts', () => {
);
});
it('handles abort signal without error event', async () => {
it('handles abort signal without error event (emits feature_complete when isAutoMode=true)', async () => {
const abortError = new Error('abort');
abortError.name = 'AbortError';
mockRunAgentFn = vi.fn().mockRejectedValue(abortError);
@@ -1136,7 +1148,7 @@ describe('execution-service.ts', () => {
mockLoadContextFilesFn
);
await svc.executeFeature('/test/project', 'feature-1');
await svc.executeFeature('/test/project', 'feature-1', false, true);
// Should emit feature_complete with stopped by user
expect(mockEventBus.emitAutoModeEvent).toHaveBeenCalledWith(
@@ -1155,6 +1167,47 @@ describe('execution-service.ts', () => {
expect(errorCalls.length).toBe(0);
});
it('handles abort signal without emitting feature_complete when isAutoMode=false', async () => {
const abortError = new Error('abort');
abortError.name = 'AbortError';
mockRunAgentFn = vi.fn().mockRejectedValue(abortError);
const svc = new ExecutionService(
mockEventBus,
mockConcurrencyManager,
mockWorktreeResolver,
mockSettingsService,
mockRunAgentFn,
mockExecutePipelineFn,
mockUpdateFeatureStatusFn,
mockLoadFeatureFn,
mockGetPlanningPromptPrefixFn,
mockSaveFeatureSummaryFn,
mockRecordLearningsFn,
mockContextExistsFn,
mockResumeFeatureFn,
mockTrackFailureFn,
mockSignalPauseFn,
mockRecordSuccessFn,
mockSaveExecutionStateFn,
mockLoadContextFilesFn
);
await svc.executeFeature('/test/project', 'feature-1', false, false);
// Should NOT emit feature_complete when isAutoMode is false
const completeCalls = vi
.mocked(mockEventBus.emitAutoModeEvent)
.mock.calls.filter((call) => call[0] === 'auto_mode_feature_complete');
expect(completeCalls.length).toBe(0);
// Should NOT emit error event (abort is not an error)
const errorCalls = vi
.mocked(mockEventBus.emitAutoModeEvent)
.mock.calls.filter((call) => call[0] === 'auto_mode_error');
expect(errorCalls.length).toBe(0);
});
it('releases running feature even on error', async () => {
const testError = new Error('Test error');
mockRunAgentFn = vi.fn().mockRejectedValue(testError);
@@ -1339,8 +1392,8 @@ describe('execution-service.ts', () => {
it('handles missing agent output gracefully', async () => {
vi.mocked(secureFs.readFile).mockRejectedValue(new Error('ENOENT'));
// Should not throw
await service.executeFeature('/test/project', 'feature-1');
// Should not throw (isAutoMode=true so event is emitted)
await service.executeFeature('/test/project', 'feature-1', false, true);
// Feature should still complete successfully
expect(mockEventBus.emitAutoModeEvent).toHaveBeenCalledWith(

View File

@@ -170,14 +170,16 @@ describe('PipelineOrchestrator', () => {
} as unknown as WorktreeResolver;
mockConcurrencyManager = {
acquire: vi.fn().mockReturnValue({
featureId: 'feature-1',
acquire: vi.fn().mockImplementation(({ featureId, isAutoMode }) => ({
featureId,
projectPath: '/test/project',
abortController: new AbortController(),
branchName: null,
worktreePath: null,
}),
isAutoMode: isAutoMode ?? false,
})),
release: vi.fn(),
getRunningFeature: vi.fn().mockReturnValue(undefined),
} as unknown as ConcurrencyManager;
mockSettingsService = null;
@@ -541,8 +543,18 @@ describe('PipelineOrchestrator', () => {
);
});
it('should emit auto_mode_feature_complete on success', async () => {
it('should emit auto_mode_feature_complete on success when isAutoMode is true', async () => {
vi.mocked(performMerge).mockResolvedValue({ success: true });
vi.mocked(mockConcurrencyManager.getRunningFeature).mockReturnValue({
featureId: 'feature-1',
projectPath: '/test/project',
abortController: new AbortController(),
branchName: null,
worktreePath: null,
isAutoMode: true,
startTime: Date.now(),
leaseCount: 1,
});
const context = createMergeContext();
await orchestrator.attemptMerge(context);
@@ -553,6 +565,19 @@ describe('PipelineOrchestrator', () => {
);
});
it('should not emit auto_mode_feature_complete on success when isAutoMode is false', async () => {
vi.mocked(performMerge).mockResolvedValue({ success: true });
vi.mocked(mockConcurrencyManager.getRunningFeature).mockReturnValue(undefined);
const context = createMergeContext();
await orchestrator.attemptMerge(context);
const completeCalls = vi
.mocked(mockEventBus.emitAutoModeEvent)
.mock.calls.filter((call) => call[0] === 'auto_mode_feature_complete');
expect(completeCalls.length).toBe(0);
});
it('should return needsAgentResolution true on conflict', async () => {
vi.mocked(performMerge).mockResolvedValue({
success: false,
@@ -623,13 +648,24 @@ describe('PipelineOrchestrator', () => {
expect(mockExecuteFeatureFn).toHaveBeenCalled();
});
it('should complete feature when step no longer exists', async () => {
it('should complete feature when step no longer exists and emit event when isAutoMode=true', async () => {
const invalidPipelineInfo: PipelineStatusInfo = {
...validPipelineInfo,
stepIndex: -1,
step: null,
};
vi.mocked(mockConcurrencyManager.getRunningFeature).mockReturnValue({
featureId: 'feature-1',
projectPath: '/test/project',
abortController: new AbortController(),
branchName: null,
worktreePath: null,
isAutoMode: true,
startTime: Date.now(),
leaseCount: 1,
});
await orchestrator.resumePipeline('/test/project', testFeature, true, invalidPipelineInfo);
expect(mockUpdateFeatureStatusFn).toHaveBeenCalledWith(
@@ -642,6 +678,28 @@ describe('PipelineOrchestrator', () => {
expect.objectContaining({ message: expect.stringContaining('no longer exists') })
);
});
it('should not emit feature_complete when step no longer exists and isAutoMode=false', async () => {
const invalidPipelineInfo: PipelineStatusInfo = {
...validPipelineInfo,
stepIndex: -1,
step: null,
};
vi.mocked(mockConcurrencyManager.getRunningFeature).mockReturnValue(undefined);
await orchestrator.resumePipeline('/test/project', testFeature, true, invalidPipelineInfo);
expect(mockUpdateFeatureStatusFn).toHaveBeenCalledWith(
'/test/project',
'feature-1',
'verified'
);
const completeCalls = vi
.mocked(mockEventBus.emitAutoModeEvent)
.mock.calls.filter((call) => call[0] === 'auto_mode_feature_complete');
expect(completeCalls.length).toBe(0);
});
});
describe('resumeFromStep', () => {
@@ -666,7 +724,7 @@ describe('PipelineOrchestrator', () => {
expect(mockRunAgentFn).toHaveBeenCalled();
});
it('should complete feature when all remaining steps excluded', async () => {
it('should complete feature when all remaining steps excluded and emit event when isAutoMode=true', async () => {
const featureWithAllExcluded: Feature = {
...testFeature,
excludedPipelineSteps: ['step-1', 'step-2'],
@@ -674,6 +732,16 @@ describe('PipelineOrchestrator', () => {
vi.mocked(pipelineService.getNextStatus).mockReturnValue('verified');
vi.mocked(pipelineService.isPipelineStatus).mockReturnValue(false);
vi.mocked(mockConcurrencyManager.getRunningFeature).mockReturnValue({
featureId: 'feature-1',
projectPath: '/test/project',
abortController: new AbortController(),
branchName: null,
worktreePath: null,
isAutoMode: true,
startTime: Date.now(),
leaseCount: 1,
});
await orchestrator.resumeFromStep(
'/test/project',
@@ -1033,7 +1101,7 @@ describe('PipelineOrchestrator', () => {
);
});
it('handles all steps excluded during resume', async () => {
it('handles all steps excluded during resume and emits event when isAutoMode=true', async () => {
const featureWithAllExcluded: Feature = {
...testFeature,
excludedPipelineSteps: ['step-1', 'step-2'],
@@ -1041,6 +1109,16 @@ describe('PipelineOrchestrator', () => {
vi.mocked(pipelineService.getNextStatus).mockReturnValue('verified');
vi.mocked(pipelineService.isPipelineStatus).mockReturnValue(false);
vi.mocked(mockConcurrencyManager.getRunningFeature).mockReturnValue({
featureId: 'feature-1',
projectPath: '/test/project',
abortController: new AbortController(),
branchName: null,
worktreePath: null,
isAutoMode: true,
startTime: Date.now(),
leaseCount: 1,
});
await orchestrator.resumeFromStep(
'/test/project',