Merge remote-tracking branch 'origin/v0.14.0rc' into feature/v0.14.0rc-1768981415660-tt2v

# Conflicts:
#	apps/ui/src/components/views/project-settings-view/config/navigation.ts
#	apps/ui/src/components/views/project-settings-view/hooks/use-project-settings-view.ts
This commit is contained in:
Shirone
2026-01-21 17:46:22 +01:00
61 changed files with 4752 additions and 213 deletions

View File

@@ -2063,6 +2063,52 @@ function createMockWorktreeAPI(): WorktreeAPI {
},
};
},
// Test runner methods
startTests: async (
worktreePath: string,
options?: { projectPath?: string; testFile?: string }
) => {
console.log('[Mock] Starting tests:', { worktreePath, options });
return {
success: true,
result: {
sessionId: 'mock-session-123',
worktreePath,
command: 'npm run test',
status: 'running' as const,
testFile: options?.testFile,
message: 'Tests started (mock)',
},
};
},
stopTests: async (sessionId: string) => {
console.log('[Mock] Stopping tests:', { sessionId });
return {
success: true,
result: {
sessionId,
message: 'Tests stopped (mock)',
},
};
},
getTestLogs: async (worktreePath?: string, sessionId?: string) => {
console.log('[Mock] Getting test logs:', { worktreePath, sessionId });
return {
success: false,
error: 'No test sessions found (mock)',
};
},
onTestRunnerEvent: (callback) => {
console.log('[Mock] Subscribing to test runner events');
// Return unsubscribe function
return () => {
console.log('[Mock] Unsubscribing from test runner events');
};
},
};
}
@@ -3412,6 +3458,11 @@ export interface Project {
* If a phase is not present, the global setting is used.
*/
phaseModelOverrides?: Partial<import('@automaker/types').PhaseModelConfig>;
/**
* Override the default model for new feature cards in this project.
* If not specified, falls back to the global defaultFeatureModel setting.
*/
defaultFeatureModel?: import('@automaker/types').PhaseModelEntry;
}
export interface TrashedProject extends Project {

View File

@@ -562,6 +562,9 @@ type EventType =
| 'dev-server:started'
| 'dev-server:output'
| 'dev-server:stopped'
| 'test-runner:started'
| 'test-runner:output'
| 'test-runner:completed'
| 'notification:created';
/**
@@ -593,6 +596,44 @@ export type DevServerLogEvent =
| { type: 'dev-server:output'; payload: DevServerOutputEvent }
| { type: 'dev-server:stopped'; payload: DevServerStoppedEvent };
/**
* Test runner event payloads for WebSocket streaming
*/
export type TestRunStatus = 'pending' | 'running' | 'passed' | 'failed' | 'cancelled' | 'error';
export interface TestRunnerStartedEvent {
sessionId: string;
worktreePath: string;
/** The test command being run (from project settings) */
command: string;
testFile?: string;
timestamp: string;
}
export interface TestRunnerOutputEvent {
sessionId: string;
worktreePath: string;
content: string;
timestamp: string;
}
export interface TestRunnerCompletedEvent {
sessionId: string;
worktreePath: string;
/** The test command that was run */
command: string;
status: TestRunStatus;
testFile?: string;
exitCode: number | null;
duration: number;
timestamp: string;
}
export type TestRunnerEvent =
| { type: 'test-runner:started'; payload: TestRunnerStartedEvent }
| { type: 'test-runner:output'; payload: TestRunnerOutputEvent }
| { type: 'test-runner:completed'; payload: TestRunnerCompletedEvent };
/**
* Response type for fetching dev server logs
*/
@@ -608,6 +649,26 @@ export interface DevServerLogsResponse {
error?: string;
}
/**
* Response type for fetching test logs
*/
export interface TestLogsResponse {
success: boolean;
result?: {
sessionId: string;
worktreePath: string;
/** The test command that was/is being run */
command: string;
status: TestRunStatus;
testFile?: string;
logs: string;
startedAt: string;
finishedAt: string | null;
exitCode: number | null;
};
error?: string;
}
type EventCallback = (payload: unknown) => void;
interface EnhancePromptResult {
@@ -2001,6 +2062,32 @@ export class HttpApiClient implements ElectronAPI {
unsub3();
};
},
// Test runner methods
startTests: (worktreePath: string, options?: { projectPath?: string; testFile?: string }) =>
this.post('/api/worktree/start-tests', { worktreePath, ...options }),
stopTests: (sessionId: string) => this.post('/api/worktree/stop-tests', { sessionId }),
getTestLogs: (worktreePath?: string, sessionId?: string): Promise<TestLogsResponse> => {
const params = new URLSearchParams();
if (worktreePath) params.append('worktreePath', worktreePath);
if (sessionId) params.append('sessionId', sessionId);
return this.get(`/api/worktree/test-logs?${params.toString()}`);
},
onTestRunnerEvent: (callback: (event: TestRunnerEvent) => void) => {
const unsub1 = this.subscribeToEvent('test-runner:started', (payload) =>
callback({ type: 'test-runner:started', payload: payload as TestRunnerStartedEvent })
);
const unsub2 = this.subscribeToEvent('test-runner:output', (payload) =>
callback({ type: 'test-runner:output', payload: payload as TestRunnerOutputEvent })
);
const unsub3 = this.subscribeToEvent('test-runner:completed', (payload) =>
callback({ type: 'test-runner:completed', payload: payload as TestRunnerCompletedEvent })
);
return () => {
unsub1();
unsub2();
unsub3();
};
},
};
// Git API
@@ -2362,6 +2449,7 @@ export class HttpApiClient implements ElectronAPI {
defaultDeleteBranchWithWorktree?: boolean;
autoDismissInitScriptIndicator?: boolean;
lastSelectedSessionId?: string;
testCommand?: string;
};
error?: string;
}> => this.post('/api/settings/project', { projectPath }),

View File

@@ -156,35 +156,23 @@ export function sanitizeForTestId(name: string): string {
/**
* Generate a UUID v4 string.
*
* Uses crypto.randomUUID() when available (secure contexts: HTTPS or localhost).
* Falls back to crypto.getRandomValues() for non-secure contexts (e.g., Docker via HTTP).
* Uses crypto.getRandomValues() which works in all modern browsers,
* including non-secure contexts (e.g., Docker via HTTP).
*
* @returns A RFC 4122 compliant UUID v4 string (e.g., "550e8400-e29b-41d4-a716-446655440000")
*/
export function generateUUID(): string {
// Use native randomUUID if available (secure contexts: HTTPS or localhost)
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
return crypto.randomUUID();
if (typeof crypto === 'undefined' || typeof crypto.getRandomValues === 'undefined') {
throw new Error('Cryptographically secure random number generator not available.');
}
const bytes = new Uint8Array(16);
crypto.getRandomValues(bytes);
// Fallback using crypto.getRandomValues() (works in all modern browsers, including non-secure contexts)
if (typeof crypto !== 'undefined' && typeof crypto.getRandomValues === 'function') {
const bytes = new Uint8Array(16);
crypto.getRandomValues(bytes);
// Set version (4) and variant (RFC 4122) bits
bytes[6] = (bytes[6] & 0x0f) | 0x40; // Version 4
bytes[8] = (bytes[8] & 0x3f) | 0x80; // Variant RFC 4122
// Set version (4) and variant (RFC 4122) bits
bytes[6] = (bytes[6] & 0x0f) | 0x40; // Version 4
bytes[8] = (bytes[8] & 0x3f) | 0x80; // Variant RFC 4122
// Convert to hex string with proper UUID format
const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('');
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
}
// Last resort fallback using Math.random() - less secure but ensures functionality
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
const r = (Math.random() * 16) | 0;
const v = c === 'x' ? r : (r & 0x3) | 0x8;
return v.toString(16);
});
// Convert to hex string with proper UUID format
const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('');
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
}