Add quick-add feature with improved workflows (#802)

* Changes from feature/quick-add

* feat: Clarify system prompt and improve error handling across services. Address PR Feedback

* feat: Improve PR description parsing and refactor event handling

* feat: Add context options to pipeline orchestrator initialization

* fix: Deduplicate React and handle CJS interop for use-sync-external-store

Resolve "Cannot read properties of null (reading 'useState')" errors by
deduplicating React/react-dom and ensuring use-sync-external-store is
bundled together with React to prevent CJS packages from resolving to
different React instances.
This commit is contained in:
gsxdsm
2026-02-22 20:48:09 -08:00
committed by GitHub
parent 9305ecc242
commit e7504b247f
70 changed files with 3141 additions and 560 deletions

View File

@@ -524,6 +524,202 @@ describe('EventHookService', () => {
});
});
describe('event mapping - feature_status_changed (non-auto-mode completion)', () => {
it('should trigger feature_success when status changes to verified', async () => {
mockFeatureLoader = createMockFeatureLoader({
'feat-1': { title: 'Manual Feature' },
});
service.initialize(
mockEmitter,
mockSettingsService,
mockEventHistoryService,
mockFeatureLoader
);
mockEmitter.simulateEvent('auto-mode:event', {
type: 'feature_status_changed',
featureId: 'feat-1',
projectPath: '/test/project',
status: 'verified',
});
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.featureName).toBe('Manual Feature');
expect(storeCall.passes).toBe(true);
});
it('should trigger feature_success when status changes to waiting_approval', async () => {
mockFeatureLoader = createMockFeatureLoader({
'feat-1': { title: 'Manual Feature' },
});
service.initialize(
mockEmitter,
mockSettingsService,
mockEventHistoryService,
mockFeatureLoader
);
mockEmitter.simulateEvent('auto-mode:event', {
type: 'feature_status_changed',
featureId: 'feat-1',
projectPath: '/test/project',
status: 'waiting_approval',
});
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);
expect(storeCall.featureName).toBe('Manual Feature');
});
it('should NOT trigger hooks for non-completion status changes', async () => {
service.initialize(
mockEmitter,
mockSettingsService,
mockEventHistoryService,
mockFeatureLoader
);
mockEmitter.simulateEvent('auto-mode:event', {
type: 'feature_status_changed',
featureId: 'feat-1',
projectPath: '/test/project',
status: 'in_progress',
});
// Give it time to process
await new Promise((resolve) => setTimeout(resolve, 50));
expect(mockEventHistoryService.storeEvent).not.toHaveBeenCalled();
});
it('should NOT double-fire hooks when auto_mode_feature_complete already fired', async () => {
service.initialize(
mockEmitter,
mockSettingsService,
mockEventHistoryService,
mockFeatureLoader
);
// First: auto_mode_feature_complete fires (auto-mode path)
mockEmitter.simulateEvent('auto-mode:event', {
type: 'auto_mode_feature_complete',
featureId: 'feat-1',
featureName: 'Auto Feature',
passes: true,
message: 'Feature completed',
projectPath: '/test/project',
});
await vi.waitFor(() => {
expect(mockEventHistoryService.storeEvent).toHaveBeenCalledTimes(1);
});
// Then: feature_status_changed fires for the same feature
mockEmitter.simulateEvent('auto-mode:event', {
type: 'feature_status_changed',
featureId: 'feat-1',
projectPath: '/test/project',
status: 'verified',
});
// Give it time to process
await new Promise((resolve) => setTimeout(resolve, 50));
// Should still only have been called once (from auto_mode_feature_complete)
expect(mockEventHistoryService.storeEvent).toHaveBeenCalledTimes(1);
});
it('should NOT double-fire hooks when auto_mode_error already fired for feature', async () => {
service.initialize(
mockEmitter,
mockSettingsService,
mockEventHistoryService,
mockFeatureLoader
);
// First: auto_mode_error fires for a feature
mockEmitter.simulateEvent('auto-mode:event', {
type: 'auto_mode_error',
featureId: 'feat-1',
error: 'Something failed',
errorType: 'execution',
projectPath: '/test/project',
});
await vi.waitFor(() => {
expect(mockEventHistoryService.storeEvent).toHaveBeenCalledTimes(1);
});
// Then: feature_status_changed fires for the same feature (e.g., reset to backlog)
mockEmitter.simulateEvent('auto-mode:event', {
type: 'feature_status_changed',
featureId: 'feat-1',
projectPath: '/test/project',
status: 'verified', // unlikely after error, but tests the dedup
});
// Give it time to process
await new Promise((resolve) => setTimeout(resolve, 50));
// Should still only have been called once
expect(mockEventHistoryService.storeEvent).toHaveBeenCalledTimes(1);
});
it('should fire hooks for different features independently', async () => {
service.initialize(
mockEmitter,
mockSettingsService,
mockEventHistoryService,
mockFeatureLoader
);
// Auto-mode completion for feat-1
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).toHaveBeenCalledTimes(1);
});
// Manual completion for feat-2 (different feature)
mockEmitter.simulateEvent('auto-mode:event', {
type: 'feature_status_changed',
featureId: 'feat-2',
projectPath: '/test/project',
status: 'verified',
});
await vi.waitFor(() => {
expect(mockEventHistoryService.storeEvent).toHaveBeenCalledTimes(2);
});
// feat-2 should have triggered feature_success
const secondCall = (mockEventHistoryService.storeEvent as ReturnType<typeof vi.fn>).mock
.calls[1][0];
expect(secondCall.trigger).toBe('feature_success');
expect(secondCall.featureId).toBe('feat-2');
});
});
describe('error context for error events', () => {
it('should use payload.error when available for error triggers', async () => {
service.initialize(

View File

@@ -34,6 +34,7 @@ import { getFeatureDir } from '@automaker/platform';
import {
getPromptCustomization,
getAutoLoadClaudeMdSetting,
getUseClaudeCodeSystemPromptSetting,
filterClaudeMdFromContext,
} from '../../../src/lib/settings-helpers.js';
import { extractSummary } from '../../../src/services/spec-parser.js';
@@ -67,6 +68,7 @@ vi.mock('../../../src/lib/settings-helpers.js', () => ({
},
}),
getAutoLoadClaudeMdSetting: vi.fn().mockResolvedValue(true),
getUseClaudeCodeSystemPromptSetting: vi.fn().mockResolvedValue(true),
filterClaudeMdFromContext: vi.fn().mockReturnValue('context prompt'),
}));
@@ -230,6 +232,7 @@ describe('execution-service.ts', () => {
},
} as Awaited<ReturnType<typeof getPromptCustomization>>);
vi.mocked(getAutoLoadClaudeMdSetting).mockResolvedValue(true);
vi.mocked(getUseClaudeCodeSystemPromptSetting).mockResolvedValue(true);
vi.mocked(filterClaudeMdFromContext).mockReturnValue('context prompt');
// Re-setup spec-parser mock

View File

@@ -57,6 +57,7 @@ vi.mock('../../../src/lib/settings-helpers.js', () => ({
},
}),
getAutoLoadClaudeMdSetting: vi.fn().mockResolvedValue(true),
getUseClaudeCodeSystemPromptSetting: vi.fn().mockResolvedValue(true),
filterClaudeMdFromContext: vi.fn().mockReturnValue('context prompt'),
}));