mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-03-18 22:33:08 +00:00
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:
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'),
|
||||
}));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user