mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-03-19 10:43:08 +00:00
Improve auto-loop event emission and add ntfy notifications (#821)
This commit is contained in:
@@ -5,6 +5,9 @@ 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';
|
||||
|
||||
// Mock global fetch for ntfy tests
|
||||
const originalFetch = global.fetch;
|
||||
|
||||
/**
|
||||
* Create a mock EventEmitter for testing
|
||||
*/
|
||||
@@ -38,9 +41,15 @@ function createMockEventEmitter(): EventEmitter & {
|
||||
/**
|
||||
* Create a mock SettingsService
|
||||
*/
|
||||
function createMockSettingsService(hooks: unknown[] = []): SettingsService {
|
||||
function createMockSettingsService(
|
||||
hooks: unknown[] = [],
|
||||
ntfyEndpoints: unknown[] = []
|
||||
): SettingsService {
|
||||
return {
|
||||
getGlobalSettings: vi.fn().mockResolvedValue({ eventHooks: hooks }),
|
||||
getGlobalSettings: vi.fn().mockResolvedValue({
|
||||
eventHooks: hooks,
|
||||
ntfyEndpoints: ntfyEndpoints,
|
||||
}),
|
||||
} as unknown as SettingsService;
|
||||
}
|
||||
|
||||
@@ -70,6 +79,7 @@ describe('EventHookService', () => {
|
||||
let mockSettingsService: ReturnType<typeof createMockSettingsService>;
|
||||
let mockEventHistoryService: ReturnType<typeof createMockEventHistoryService>;
|
||||
let mockFeatureLoader: ReturnType<typeof createMockFeatureLoader>;
|
||||
let mockFetch: ReturnType<typeof vi.fn>;
|
||||
|
||||
beforeEach(() => {
|
||||
service = new EventHookService();
|
||||
@@ -77,10 +87,14 @@ describe('EventHookService', () => {
|
||||
mockSettingsService = createMockSettingsService();
|
||||
mockEventHistoryService = createMockEventHistoryService();
|
||||
mockFeatureLoader = createMockFeatureLoader();
|
||||
// Set up mock fetch for ntfy tests
|
||||
mockFetch = vi.fn();
|
||||
global.fetch = mockFetch;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
service.destroy();
|
||||
global.fetch = originalFetch;
|
||||
});
|
||||
|
||||
describe('initialize', () => {
|
||||
@@ -832,4 +846,628 @@ describe('EventHookService', () => {
|
||||
expect(storeCall.error).toBe('Feature stopped by user');
|
||||
});
|
||||
});
|
||||
|
||||
describe('ntfy hook execution', () => {
|
||||
const mockNtfyEndpoint = {
|
||||
id: 'endpoint-1',
|
||||
name: 'Test Endpoint',
|
||||
serverUrl: 'https://ntfy.sh',
|
||||
topic: 'test-topic',
|
||||
authType: 'none' as const,
|
||||
enabled: true,
|
||||
};
|
||||
|
||||
it('should execute ntfy hook when endpoint is configured', async () => {
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
status: 200,
|
||||
});
|
||||
|
||||
const hooks = [
|
||||
{
|
||||
id: 'ntfy-hook-1',
|
||||
enabled: true,
|
||||
trigger: 'feature_success',
|
||||
name: 'Ntfy Success Hook',
|
||||
action: {
|
||||
type: 'ntfy',
|
||||
endpointId: 'endpoint-1',
|
||||
title: 'Feature {{featureName}} completed!',
|
||||
priority: 3,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
mockSettingsService = createMockSettingsService(hooks, [mockNtfyEndpoint]);
|
||||
service.initialize(
|
||||
mockEmitter,
|
||||
mockSettingsService,
|
||||
mockEventHistoryService,
|
||||
mockFeatureLoader
|
||||
);
|
||||
|
||||
mockEmitter.simulateEvent('auto-mode:event', {
|
||||
type: 'auto_mode_feature_complete',
|
||||
executionMode: 'auto',
|
||||
featureId: 'feat-1',
|
||||
featureName: 'Test Feature',
|
||||
passes: true,
|
||||
message: 'Feature completed',
|
||||
projectPath: '/test/project',
|
||||
});
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(mockFetch).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
const [url, options] = mockFetch.mock.calls[0];
|
||||
expect(url).toBe('https://ntfy.sh/test-topic');
|
||||
expect(options.method).toBe('POST');
|
||||
expect(options.headers['Title']).toBe('Feature Test Feature completed!');
|
||||
});
|
||||
|
||||
it('should NOT execute ntfy hook when endpoint is not found', async () => {
|
||||
const hooks = [
|
||||
{
|
||||
id: 'ntfy-hook-1',
|
||||
enabled: true,
|
||||
trigger: 'feature_success',
|
||||
name: 'Ntfy Hook with Missing Endpoint',
|
||||
action: {
|
||||
type: 'ntfy',
|
||||
endpointId: 'non-existent-endpoint',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
mockSettingsService = createMockSettingsService(hooks, [mockNtfyEndpoint]);
|
||||
service.initialize(
|
||||
mockEmitter,
|
||||
mockSettingsService,
|
||||
mockEventHistoryService,
|
||||
mockFeatureLoader
|
||||
);
|
||||
|
||||
mockEmitter.simulateEvent('auto-mode:event', {
|
||||
type: 'auto_mode_feature_complete',
|
||||
executionMode: 'auto',
|
||||
featureId: 'feat-1',
|
||||
featureName: 'Test Feature',
|
||||
passes: true,
|
||||
message: 'Feature completed',
|
||||
projectPath: '/test/project',
|
||||
});
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(mockEventHistoryService.storeEvent).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// Fetch should NOT have been called since endpoint doesn't exist
|
||||
expect(mockFetch).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should use ntfy endpoint default values when hook does not override', async () => {
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
status: 200,
|
||||
});
|
||||
|
||||
const endpointWithDefaults = {
|
||||
...mockNtfyEndpoint,
|
||||
defaultTags: 'default-tag',
|
||||
defaultEmoji: 'tada',
|
||||
defaultClickUrl: 'https://default.example.com',
|
||||
};
|
||||
|
||||
const hooks = [
|
||||
{
|
||||
id: 'ntfy-hook-1',
|
||||
enabled: true,
|
||||
trigger: 'feature_error',
|
||||
name: 'Ntfy Error Hook',
|
||||
action: {
|
||||
type: 'ntfy',
|
||||
endpointId: 'endpoint-1',
|
||||
// No title, tags, or emoji - should use endpoint defaults
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
mockSettingsService = createMockSettingsService(hooks, [endpointWithDefaults]);
|
||||
service.initialize(
|
||||
mockEmitter,
|
||||
mockSettingsService,
|
||||
mockEventHistoryService,
|
||||
mockFeatureLoader
|
||||
);
|
||||
|
||||
mockEmitter.simulateEvent('auto-mode:event', {
|
||||
type: 'auto_mode_feature_complete',
|
||||
executionMode: 'auto',
|
||||
featureId: 'feat-1',
|
||||
featureName: 'Failed Feature',
|
||||
passes: false,
|
||||
message: 'Something went wrong',
|
||||
projectPath: '/test/project',
|
||||
});
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(mockFetch).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
const options = mockFetch.mock.calls[0][1];
|
||||
// Should use default tags and emoji from endpoint
|
||||
expect(options.headers['Tags']).toBe('tada,default-tag');
|
||||
// Click URL gets deep-link query param when feature context is available
|
||||
expect(options.headers['Click']).toContain('https://default.example.com/board');
|
||||
expect(options.headers['Click']).toContain('featureId=feat-1');
|
||||
});
|
||||
|
||||
it('should send ntfy notification with authentication', async () => {
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
status: 200,
|
||||
});
|
||||
|
||||
const endpointWithAuth = {
|
||||
...mockNtfyEndpoint,
|
||||
authType: 'token' as const,
|
||||
token: 'tk_test_token',
|
||||
};
|
||||
|
||||
const hooks = [
|
||||
{
|
||||
id: 'ntfy-hook-1',
|
||||
enabled: true,
|
||||
trigger: 'feature_success',
|
||||
name: 'Authenticated Ntfy Hook',
|
||||
action: {
|
||||
type: 'ntfy',
|
||||
endpointId: 'endpoint-1',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
mockSettingsService = createMockSettingsService(hooks, [endpointWithAuth]);
|
||||
service.initialize(
|
||||
mockEmitter,
|
||||
mockSettingsService,
|
||||
mockEventHistoryService,
|
||||
mockFeatureLoader
|
||||
);
|
||||
|
||||
mockEmitter.simulateEvent('auto-mode:event', {
|
||||
type: 'auto_mode_feature_complete',
|
||||
executionMode: 'auto',
|
||||
featureId: 'feat-1',
|
||||
featureName: 'Test Feature',
|
||||
passes: true,
|
||||
message: 'Feature completed',
|
||||
projectPath: '/test/project',
|
||||
});
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(mockFetch).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
const options = mockFetch.mock.calls[0][1];
|
||||
expect(options.headers['Authorization']).toBe('Bearer tk_test_token');
|
||||
});
|
||||
|
||||
it('should handle ntfy notification failure gracefully', async () => {
|
||||
mockFetch.mockRejectedValueOnce(new Error('Network error'));
|
||||
|
||||
const hooks = [
|
||||
{
|
||||
id: 'ntfy-hook-1',
|
||||
enabled: true,
|
||||
trigger: 'feature_success',
|
||||
name: 'Ntfy Hook That Will Fail',
|
||||
action: {
|
||||
type: 'ntfy',
|
||||
endpointId: 'endpoint-1',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
mockSettingsService = createMockSettingsService(hooks, [mockNtfyEndpoint]);
|
||||
service.initialize(
|
||||
mockEmitter,
|
||||
mockSettingsService,
|
||||
mockEventHistoryService,
|
||||
mockFeatureLoader
|
||||
);
|
||||
|
||||
mockEmitter.simulateEvent('auto-mode:event', {
|
||||
type: 'auto_mode_feature_complete',
|
||||
executionMode: 'auto',
|
||||
featureId: 'feat-1',
|
||||
featureName: 'Test Feature',
|
||||
passes: true,
|
||||
message: 'Feature completed',
|
||||
projectPath: '/test/project',
|
||||
});
|
||||
|
||||
// Should not throw - error should be caught gracefully
|
||||
await vi.waitFor(() => {
|
||||
expect(mockFetch).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// Event should still be stored even if ntfy hook fails
|
||||
expect(mockEventHistoryService.storeEvent).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should substitute variables in ntfy title and body', async () => {
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
status: 200,
|
||||
});
|
||||
|
||||
const hooks = [
|
||||
{
|
||||
id: 'ntfy-hook-1',
|
||||
enabled: true,
|
||||
trigger: 'feature_success',
|
||||
name: 'Ntfy Hook with Variables',
|
||||
action: {
|
||||
type: 'ntfy',
|
||||
endpointId: 'endpoint-1',
|
||||
title: '[{{projectName}}] {{featureName}}',
|
||||
body: 'Feature {{featureId}} completed at {{timestamp}}',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
mockSettingsService = createMockSettingsService(hooks, [mockNtfyEndpoint]);
|
||||
service.initialize(
|
||||
mockEmitter,
|
||||
mockSettingsService,
|
||||
mockEventHistoryService,
|
||||
mockFeatureLoader
|
||||
);
|
||||
|
||||
mockEmitter.simulateEvent('auto-mode:event', {
|
||||
type: 'auto_mode_feature_complete',
|
||||
executionMode: 'auto',
|
||||
featureId: 'feat-123',
|
||||
featureName: 'Cool Feature',
|
||||
passes: true,
|
||||
message: 'Feature completed',
|
||||
projectPath: '/test/my-project',
|
||||
projectName: 'my-project',
|
||||
});
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(mockFetch).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
const options = mockFetch.mock.calls[0][1];
|
||||
expect(options.headers['Title']).toBe('[my-project] Cool Feature');
|
||||
expect(options.body).toContain('feat-123');
|
||||
});
|
||||
|
||||
it('should NOT execute ntfy hook when endpoint is disabled', async () => {
|
||||
const disabledEndpoint = {
|
||||
...mockNtfyEndpoint,
|
||||
enabled: false,
|
||||
};
|
||||
|
||||
const hooks = [
|
||||
{
|
||||
id: 'ntfy-hook-1',
|
||||
enabled: true,
|
||||
trigger: 'feature_success',
|
||||
name: 'Ntfy Hook with Disabled Endpoint',
|
||||
action: {
|
||||
type: 'ntfy',
|
||||
endpointId: 'endpoint-1',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
mockSettingsService = createMockSettingsService(hooks, [disabledEndpoint]);
|
||||
service.initialize(
|
||||
mockEmitter,
|
||||
mockSettingsService,
|
||||
mockEventHistoryService,
|
||||
mockFeatureLoader
|
||||
);
|
||||
|
||||
mockEmitter.simulateEvent('auto-mode:event', {
|
||||
type: 'auto_mode_feature_complete',
|
||||
executionMode: 'auto',
|
||||
featureId: 'feat-1',
|
||||
featureName: 'Test Feature',
|
||||
passes: true,
|
||||
message: 'Feature completed',
|
||||
projectPath: '/test/project',
|
||||
});
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(mockEventHistoryService.storeEvent).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// Fetch should not be called because endpoint is disabled
|
||||
expect(mockFetch).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should use hook-specific values over endpoint defaults', async () => {
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
status: 200,
|
||||
});
|
||||
|
||||
const endpointWithDefaults = {
|
||||
...mockNtfyEndpoint,
|
||||
defaultTags: 'default-tag',
|
||||
defaultEmoji: 'default-emoji',
|
||||
defaultClickUrl: 'https://default.example.com',
|
||||
};
|
||||
|
||||
const hooks = [
|
||||
{
|
||||
id: 'ntfy-hook-1',
|
||||
enabled: true,
|
||||
trigger: 'feature_success',
|
||||
name: 'Ntfy Hook with Overrides',
|
||||
action: {
|
||||
type: 'ntfy',
|
||||
endpointId: 'endpoint-1',
|
||||
tags: 'override-tag',
|
||||
emoji: 'override-emoji',
|
||||
clickUrl: 'https://override.example.com',
|
||||
priority: 5,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
mockSettingsService = createMockSettingsService(hooks, [endpointWithDefaults]);
|
||||
service.initialize(
|
||||
mockEmitter,
|
||||
mockSettingsService,
|
||||
mockEventHistoryService,
|
||||
mockFeatureLoader
|
||||
);
|
||||
|
||||
mockEmitter.simulateEvent('auto-mode:event', {
|
||||
type: 'auto_mode_feature_complete',
|
||||
executionMode: 'auto',
|
||||
featureId: 'feat-1',
|
||||
featureName: 'Test Feature',
|
||||
passes: true,
|
||||
message: 'Feature completed',
|
||||
projectPath: '/test/project',
|
||||
});
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(mockFetch).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
const options = mockFetch.mock.calls[0][1];
|
||||
// Hook values should override endpoint defaults
|
||||
expect(options.headers['Tags']).toBe('override-emoji,override-tag');
|
||||
expect(options.headers['Click']).toBe('https://override.example.com');
|
||||
expect(options.headers['Priority']).toBe('5');
|
||||
});
|
||||
|
||||
describe('click URL deep linking', () => {
|
||||
it('should generate board URL with featureId query param when feature context is available', async () => {
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
status: 200,
|
||||
});
|
||||
|
||||
const endpointWithDefaultClickUrl = {
|
||||
...mockNtfyEndpoint,
|
||||
defaultClickUrl: 'https://app.example.com',
|
||||
};
|
||||
|
||||
const hooks = [
|
||||
{
|
||||
id: 'ntfy-hook-1',
|
||||
enabled: true,
|
||||
trigger: 'feature_success',
|
||||
name: 'Ntfy Hook',
|
||||
action: {
|
||||
type: 'ntfy',
|
||||
endpointId: 'endpoint-1',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
mockSettingsService = createMockSettingsService(hooks, [endpointWithDefaultClickUrl]);
|
||||
service.initialize(
|
||||
mockEmitter,
|
||||
mockSettingsService,
|
||||
mockEventHistoryService,
|
||||
mockFeatureLoader
|
||||
);
|
||||
|
||||
mockEmitter.simulateEvent('auto-mode:event', {
|
||||
type: 'auto_mode_feature_complete',
|
||||
executionMode: 'auto',
|
||||
featureId: 'test-feature-123',
|
||||
featureName: 'Test Feature',
|
||||
passes: true,
|
||||
message: 'Feature completed',
|
||||
projectPath: '/test/project',
|
||||
});
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(mockFetch).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
const options = mockFetch.mock.calls[0][1];
|
||||
const clickUrl = options.headers['Click'];
|
||||
|
||||
// Should use /board path with featureId query param
|
||||
expect(clickUrl).toContain('/board');
|
||||
expect(clickUrl).toContain('featureId=test-feature-123');
|
||||
// Should NOT use the old path-based format
|
||||
expect(clickUrl).not.toContain('/feature/');
|
||||
});
|
||||
|
||||
it('should generate board URL without featureId when no feature context', async () => {
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
status: 200,
|
||||
});
|
||||
|
||||
const endpointWithDefaultClickUrl = {
|
||||
...mockNtfyEndpoint,
|
||||
defaultClickUrl: 'https://app.example.com',
|
||||
};
|
||||
|
||||
const hooks = [
|
||||
{
|
||||
id: 'ntfy-hook-1',
|
||||
enabled: true,
|
||||
trigger: 'auto_mode_complete',
|
||||
name: 'Auto Mode Complete Hook',
|
||||
action: {
|
||||
type: 'ntfy',
|
||||
endpointId: 'endpoint-1',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
mockSettingsService = createMockSettingsService(hooks, [endpointWithDefaultClickUrl]);
|
||||
service.initialize(
|
||||
mockEmitter,
|
||||
mockSettingsService,
|
||||
mockEventHistoryService,
|
||||
mockFeatureLoader
|
||||
);
|
||||
|
||||
// Event without featureId but with projectPath (auto_mode_idle triggers auto_mode_complete)
|
||||
mockEmitter.simulateEvent('auto-mode:event', {
|
||||
type: 'auto_mode_idle',
|
||||
executionMode: 'auto',
|
||||
projectPath: '/test/project',
|
||||
totalFeatures: 5,
|
||||
});
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(mockFetch).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
const options = mockFetch.mock.calls[0][1];
|
||||
const clickUrl = options.headers['Click'];
|
||||
|
||||
// Should navigate to board without featureId
|
||||
expect(clickUrl).toContain('/board');
|
||||
expect(clickUrl).not.toContain('featureId=');
|
||||
});
|
||||
|
||||
it('should use hook-specific click URL overriding default with featureId', async () => {
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
status: 200,
|
||||
});
|
||||
|
||||
const endpointWithDefaultClickUrl = {
|
||||
...mockNtfyEndpoint,
|
||||
defaultClickUrl: 'https://default.example.com',
|
||||
};
|
||||
|
||||
const hooks = [
|
||||
{
|
||||
id: 'ntfy-hook-1',
|
||||
enabled: true,
|
||||
trigger: 'feature_success',
|
||||
name: 'Ntfy Hook with Custom Click URL',
|
||||
action: {
|
||||
type: 'ntfy',
|
||||
endpointId: 'endpoint-1',
|
||||
clickUrl: 'https://custom.example.com/custom-page',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
mockSettingsService = createMockSettingsService(hooks, [endpointWithDefaultClickUrl]);
|
||||
service.initialize(
|
||||
mockEmitter,
|
||||
mockSettingsService,
|
||||
mockEventHistoryService,
|
||||
mockFeatureLoader
|
||||
);
|
||||
|
||||
mockEmitter.simulateEvent('auto-mode:event', {
|
||||
type: 'auto_mode_feature_complete',
|
||||
executionMode: 'auto',
|
||||
featureId: 'feat-789',
|
||||
featureName: 'Custom URL Test',
|
||||
passes: true,
|
||||
message: 'Feature completed',
|
||||
projectPath: '/test/project',
|
||||
});
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(mockFetch).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
const options = mockFetch.mock.calls[0][1];
|
||||
const clickUrl = options.headers['Click'];
|
||||
|
||||
// Should use the hook-specific click URL (not modified with featureId since it's a custom URL)
|
||||
expect(clickUrl).toBe('https://custom.example.com/custom-page');
|
||||
});
|
||||
|
||||
it('should preserve existing query params when adding featureId', async () => {
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
status: 200,
|
||||
});
|
||||
|
||||
const endpointWithDefaultClickUrl = {
|
||||
...mockNtfyEndpoint,
|
||||
defaultClickUrl: 'https://app.example.com/board?view=list',
|
||||
};
|
||||
|
||||
const hooks = [
|
||||
{
|
||||
id: 'ntfy-hook-1',
|
||||
enabled: true,
|
||||
trigger: 'feature_success',
|
||||
name: 'Ntfy Hook',
|
||||
action: {
|
||||
type: 'ntfy',
|
||||
endpointId: 'endpoint-1',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
mockSettingsService = createMockSettingsService(hooks, [endpointWithDefaultClickUrl]);
|
||||
service.initialize(
|
||||
mockEmitter,
|
||||
mockSettingsService,
|
||||
mockEventHistoryService,
|
||||
mockFeatureLoader
|
||||
);
|
||||
|
||||
mockEmitter.simulateEvent('auto-mode:event', {
|
||||
type: 'auto_mode_feature_complete',
|
||||
executionMode: 'auto',
|
||||
featureId: 'feat-456',
|
||||
featureName: 'Test Feature',
|
||||
passes: true,
|
||||
message: 'Feature completed',
|
||||
projectPath: '/test/project',
|
||||
});
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(mockFetch).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
const options = mockFetch.mock.calls[0][1];
|
||||
const clickUrl = options.headers['Click'];
|
||||
|
||||
// Should preserve existing query params and add featureId
|
||||
expect(clickUrl).toContain('view=list');
|
||||
expect(clickUrl).toContain('featureId=feat-456');
|
||||
// Should be properly formatted URL
|
||||
expect(clickUrl).toMatch(/^https:\/\/app\.example\.com\/board\?.+$/);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user