From 515aef71bb753717f34ad363de1db5c806debdab Mon Sep 17 00:00:00 2001 From: Marc Mintel Date: Sat, 22 Nov 2025 16:55:13 +0100 Subject: [PATCH] fix(companion): resolve fixtures path correctly in Electron runtime --- apps/companion/main/di-container.ts | 45 +++++++++---- .../companion/di-container.test.ts | 66 +++++++++++++++++++ 2 files changed, 97 insertions(+), 14 deletions(-) create mode 100644 tests/integration/companion/di-container.test.ts diff --git a/apps/companion/main/di-container.ts b/apps/companion/main/di-container.ts index ebb550209..91d0697b7 100644 --- a/apps/companion/main/di-container.ts +++ b/apps/companion/main/di-container.ts @@ -18,6 +18,33 @@ import type { IAutomationEngine } from '@/packages/application/ports/IAutomation import type { ILogger } from '@/packages/application/ports/ILogger'; import type { IFixtureServerService } from '@/packages/infrastructure/adapters/automation/FixtureServerService'; +/** + * Resolve the fixtures path relative to the monorepo root. + * In Electron, app.getAppPath() returns the path to the app's main directory + * (e.g., apps/companion/dist/main in development, or the asar in production). + * We navigate up to find the monorepo root and then join with the resources path. + * + * Path breakdown from apps/companion/dist/main: + * - Level 1: apps/companion/dist (../) + * - Level 2: apps/companion (../../) + * - Level 3: apps (../../../) + * - Level 4: gridpilot (monorepo root) (../../../../) + * + * @param configuredPath - The path configured (may be relative or absolute) + * @param appPath - The app path from electron (app.getAppPath()) + * @returns Resolved absolute path + */ +export function resolveFixturesPath(configuredPath: string, appPath: string): string { + if (path.isAbsolute(configuredPath)) { + return configuredPath; + } + + const projectRoot = path.resolve(appPath, '../../../../'); + const resolvedPath = path.join(projectRoot, configuredPath); + + return resolvedPath; +} + export interface BrowserConnectionResult { success: boolean; error?: string; @@ -169,20 +196,10 @@ export class DIContainer { /** * Resolve the fixtures path relative to the monorepo root. - * In Electron, app.getAppPath() returns the path to the app's main directory - * (e.g., apps/companion/dist/main in development, or the asar in production). - * We navigate up to find the monorepo root and then join with the resources path. + * Uses the exported resolveFixturesPath function with app.getAppPath(). */ - private resolveFixturesPath(configuredPath: string): string { - if (path.isAbsolute(configuredPath)) { - return configuredPath; - } - - const appPath = app.getAppPath(); - const projectRoot = path.resolve(appPath, '../../../'); - const resolvedPath = path.join(projectRoot, configuredPath); - - return resolvedPath; + private resolveFixturesPathInternal(configuredPath: string): string { + return resolveFixturesPath(configuredPath, app.getAppPath()); } /** @@ -205,7 +222,7 @@ export class DIContainer { this.fixtureServer = new FixtureServerService(); const port = config.fixtureServer.port; - const fixturesPath = this.resolveFixturesPath(config.fixtureServer.fixturesPath); + const fixturesPath = this.resolveFixturesPathInternal(config.fixtureServer.fixturesPath); this.logger.debug('Fixture server path resolution', { configuredPath: config.fixtureServer.fixturesPath, diff --git a/tests/integration/companion/di-container.test.ts b/tests/integration/companion/di-container.test.ts new file mode 100644 index 000000000..9a548b53f --- /dev/null +++ b/tests/integration/companion/di-container.test.ts @@ -0,0 +1,66 @@ +import { describe, it, expect } from 'vitest'; +import * as path from 'path'; +import { resolveFixturesPath } from '@/apps/companion/main/di-container'; + +describe('DIContainer path resolution', () => { + describe('resolveFixturesPath', () => { + it('should resolve fixtures path to monorepo root when app path is apps/companion/dist/main', () => { + // Given: The app path as it would be in Electron runtime (apps/companion/dist/main) + const mockAppPath = '/Users/test/Projects/gridpilot/apps/companion/dist/main'; + const relativePath = './resources/iracing-hosted-sessions'; + + // When: Resolving the fixtures path + const resolved = resolveFixturesPath(relativePath, mockAppPath); + + // Then: Should resolve to monorepo root (4 levels up from apps/companion/dist/main) + // Level 0: apps/companion/dist/main (app.getAppPath()) + // Level 1: apps/companion/dist (../) + // Level 2: apps/companion (../../) + // Level 3: apps (../../../) + // Level 4: gridpilot (monorepo root) (../../../../) ← CORRECT + const expectedPath = '/Users/test/Projects/gridpilot/resources/iracing-hosted-sessions'; + expect(resolved).toBe(expectedPath); + }); + + it('should return absolute paths unchanged', () => { + // Given: An absolute path + const absolutePath = '/some/absolute/path/to/fixtures'; + const mockAppPath = '/Users/test/Projects/gridpilot/apps/companion/dist/main'; + + // When: Resolving an absolute path + const resolved = resolveFixturesPath(absolutePath, mockAppPath); + + // Then: Should return the absolute path unchanged + expect(resolved).toBe(absolutePath); + }); + + it('should navigate exactly 4 levels up from app path', () => { + // Given: A path that demonstrates the 4-level navigation + const mockAppPath = '/level4/level3/level2/level1'; + const relativePath = './target'; + + // When: Resolving the fixtures path + const resolved = resolveFixturesPath(relativePath, mockAppPath); + + // Then: Should resolve to the path 4 levels up (root /) + // Level 0: /level4/level3/level2/level1 (appPath) + // Level 1: /level4/level3/level2 (../) + // Level 2: /level4/level3 (../../) + // Level 3: /level4 (../../../) + // Level 4: / (../../../../) ← monorepo root equivalent + expect(resolved).toBe('/target'); + }); + + it('should work with different relative path formats', () => { + // Given: Various relative path formats + const mockAppPath = '/Users/test/Projects/gridpilot/apps/companion/dist/main'; + + // When/Then: Different relative formats should all work + expect(resolveFixturesPath('resources/fixtures', mockAppPath)) + .toBe('/Users/test/Projects/gridpilot/resources/fixtures'); + + expect(resolveFixturesPath('./resources/fixtures', mockAppPath)) + .toBe('/Users/test/Projects/gridpilot/resources/fixtures'); + }); + }); +}); \ No newline at end of file