diff --git a/src/mcp/tools/session-management/__tests__/session_clear_defaults.test.ts b/src/mcp/tools/session-management/__tests__/session_clear_defaults.test.ts index a5fa8a147..e28f5d676 100644 --- a/src/mcp/tools/session-management/__tests__/session_clear_defaults.test.ts +++ b/src/mcp/tools/session-management/__tests__/session_clear_defaults.test.ts @@ -44,7 +44,8 @@ describe('session-clear-defaults tool', () => { const current = sessionStore.getAll(); expect(current.scheme).toBeUndefined(); expect(current.deviceId).toBeUndefined(); - expect(current.derivedDataPath).toBeUndefined(); + // derivedDataPath is computed from projectPath when not explicitly set + expect(current.derivedDataPath).toContain('proj-'); expect(current.projectPath).toBe('/path/to/proj.xcodeproj'); expect(current.simulatorName).toBe('iPhone 17'); expect(current.useLatestOS).toBe(true); diff --git a/src/utils/__tests__/session-store.test.ts b/src/utils/__tests__/session-store.test.ts index 7d45c6a2f..4488394c4 100644 --- a/src/utils/__tests__/session-store.test.ts +++ b/src/utils/__tests__/session-store.test.ts @@ -121,4 +121,49 @@ describe('SessionStore', () => { const stored = sessionStore.getAll(); expect(stored.env).toEqual({ API_KEY: 'secret' }); }); + + it('computes a workspace-scoped derivedDataPath when workspacePath is set', () => { + sessionStore.setDefaults({ workspacePath: '/Users/dev/clone-1/MyApp.xcworkspace' }); + + const defaults = sessionStore.getAll(); + expect(defaults.derivedDataPath).toMatch(/MyApp-[a-f0-9]{12}$/); + }); + + it('computes a project-scoped derivedDataPath when projectPath is set', () => { + sessionStore.setDefaults({ projectPath: '/Users/dev/clone-2/MyApp.xcodeproj' }); + + const defaults = sessionStore.getAll(); + expect(defaults.derivedDataPath).toMatch(/MyApp-[a-f0-9]{12}$/); + }); + + it('does not override an explicitly set derivedDataPath', () => { + sessionStore.setDefaults({ + workspacePath: '/Users/dev/clone-1/MyApp.xcworkspace', + derivedDataPath: '/custom/path', + }); + + expect(sessionStore.getAll().derivedDataPath).toBe('/custom/path'); + }); + + it('produces different hashes for different workspace paths', () => { + sessionStore.setDefaults({ workspacePath: '/clone-1/MyApp.xcworkspace' }); + const path1 = sessionStore.getAll().derivedDataPath; + + sessionStore.clearAll(); + sessionStore.setDefaults({ workspacePath: '/clone-2/MyApp.xcworkspace' }); + const path2 = sessionStore.getAll().derivedDataPath; + + expect(path1).not.toBe(path2); + }); + + it('recomputes derivedDataPath when workspacePath changes', () => { + sessionStore.setDefaults({ workspacePath: '/clone-1/MyApp.xcworkspace' }); + const path1 = sessionStore.getAll().derivedDataPath; + + sessionStore.setDefaults({ workspacePath: '/clone-2/MyApp.xcworkspace' }); + const path2 = sessionStore.getAll().derivedDataPath; + + expect(path1).not.toBe(path2); + expect(path2).toMatch(/MyApp-[a-f0-9]{12}$/); + }); }); diff --git a/src/utils/build-preflight.ts b/src/utils/build-preflight.ts index 2f3c2d4f0..9c393ed57 100644 --- a/src/utils/build-preflight.ts +++ b/src/utils/build-preflight.ts @@ -1,6 +1,7 @@ import path from 'node:path'; import os from 'node:os'; import { resolveEffectiveDerivedDataPath } from './derived-data-path.ts'; +import { sessionStore } from './session-store.ts'; export interface ToolPreflightParams { operation: @@ -94,7 +95,7 @@ export function formatToolPreflight(params: ToolPreflightParams): string { } lines.push( - ` Derived Data: ${displayPath(resolveEffectiveDerivedDataPath(params.derivedDataPath))}`, + ` Derived Data: ${displayPath(resolveEffectiveDerivedDataPath(params.derivedDataPath ?? sessionStore.get('derivedDataPath')))}`, ); if (params.arch) { diff --git a/src/utils/session-store.ts b/src/utils/session-store.ts index 2d1cdf1be..4082167be 100644 --- a/src/utils/session-store.ts +++ b/src/utils/session-store.ts @@ -1,3 +1,6 @@ +import * as crypto from 'node:crypto'; +import * as path from 'node:path'; +import { DERIVED_DATA_DIR } from './log-paths.ts'; import { log } from './logger.ts'; export type SessionDefaults = { @@ -76,7 +79,7 @@ class SessionStore { } setDefaultsForProfile(profile: string | null, partial: Partial): void { - const previous = this.getAllForProfile(profile); + const previous = this.getRawForProfile(profile); const next = { ...previous, ...partial }; this.setDefaultsForResolvedProfile(profile, next); this.revision += 1; @@ -114,7 +117,7 @@ class SessionStore { return; } - const next = this.getAllForProfile(profile); + const next = this.getRawForProfile(profile); for (const k of keys) delete next[k]; this.setDefaultsForResolvedProfile(profile, next); @@ -132,6 +135,22 @@ class SessionStore { } getAllForProfile(profile: string | null): SessionDefaults { + const result = this.getRawForProfile(profile); + + if (!result.derivedDataPath) { + const anchor = result.workspacePath ?? result.projectPath; + if (anchor) { + const resolved = path.resolve(anchor); + const hash = crypto.createHash('sha256').update(resolved).digest('hex').slice(0, 12); + const name = path.basename(resolved, path.extname(resolved)); + result.derivedDataPath = path.join(DERIVED_DATA_DIR, `${name}-${hash}`); + } + } + + return result; + } + + private getRawForProfile(profile: string | null): SessionDefaults { const defaults = profile === null ? this.globalDefaults : (this.profiles[profile] ?? {}); return this.cloneDefaults(defaults); } diff --git a/src/utils/xcodebuild-pipeline.ts b/src/utils/xcodebuild-pipeline.ts index f2c0d3671..5f9752fad 100644 --- a/src/utils/xcodebuild-pipeline.ts +++ b/src/utils/xcodebuild-pipeline.ts @@ -8,6 +8,7 @@ import { createXcodebuildRunState } from './xcodebuild-run-state.ts'; import type { XcodebuildRunState } from './xcodebuild-run-state.ts'; import { displayPath } from './build-preflight.ts'; import { resolveEffectiveDerivedDataPath } from './derived-data-path.ts'; +import { sessionStore } from './session-store.ts'; import { formatDeviceId } from './device-name-resolver.ts'; import { createLogCapture, createParserDebugCapture } from './xcodebuild-log-capture.ts'; import { log as appLog } from './logging/index.ts'; @@ -158,7 +159,8 @@ function buildHeaderParams( // Always show Derived Data even if not explicitly provided if (!result.some((r) => r.label === 'Derived Data')) { - result.push({ label: 'Derived Data', value: displayPath(resolveEffectiveDerivedDataPath()) }); + const effectivePath = resolveEffectiveDerivedDataPath(sessionStore.get('derivedDataPath')); + result.push({ label: 'Derived Data', value: displayPath(effectivePath) }); } return result;