diff --git a/src/main/index.ts b/src/main/index.ts index 7bb8487d4..481eb6885 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -90,6 +90,7 @@ import { secureWindow } from './security' import { SafeModeFrame } from './safe-mode-frame' import { desktopResourceUrl, installDesktopProtocol, registerDesktopScheme, SAFE_MODE_PAGE } from './desktop-protocol' import { ensureLaunchRoot } from './state/launch-root' +import { initializeDesktopInstall } from './state/desktop-startup-install' import { listInstalledProfilePlugins, pruneUnresolvableProfileBundles, @@ -3557,7 +3558,13 @@ if (isDaemonLaunch(process.env, process.platform)) { // and the splash instead of blocking the main process right before the // Harness spawn. Only the instance that will actually launch pays for it. void prewarmShellEnvironment() - initializeDesktopService() + // Classify before DesktopService creates installation.json and bootstrap + // creates launch-root; either path would otherwise look like legacy data. + initializeDesktopInstall({ + userDataPath: app.getPath('userData'), + appVersion: app.getVersion(), + developmentBuild + }, initializeDesktopService) app.on('second-instance', (_event, argv) => { if (!isUserInitiatedInstance(argv)) return if (shouldStartInSafeMode(argv)) { diff --git a/src/main/state/desktop-startup-install.ts b/src/main/state/desktop-startup-install.ts new file mode 100644 index 000000000..c1638d2cb --- /dev/null +++ b/src/main/state/desktop-startup-install.ts @@ -0,0 +1,10 @@ +import { classifyDesktopInstall, type ClassifyDesktopInstallOptions } from './desktop-install-state' + +/** Persist the first-install decision before startup services create legacy evidence. */ +export function initializeDesktopInstall( + options: ClassifyDesktopInstallOptions, + initializeService: () => void +): void { + classifyDesktopInstall(options) + initializeService() +} diff --git a/test/desktop-startup-install.test.ts b/test/desktop-startup-install.test.ts new file mode 100644 index 000000000..1be80a939 --- /dev/null +++ b/test/desktop-startup-install.test.ts @@ -0,0 +1,53 @@ +import { mkdir, mkdtemp, readFile, rm } from 'node:fs/promises' +import { mkdirSync, readFileSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { afterEach, describe, expect, it } from 'vitest' +import { initializeDesktopInstall } from '../src/main/state/desktop-startup-install' +import { desktopInstallStatePath } from '../src/main/state/desktop-install-state' + +const roots: string[] = [] + +async function userData(): Promise { + const root = await mkdtemp(join(tmpdir(), 'dsh-startup-install-')) + roots.push(root) + return root +} + +afterEach(async () => { + await Promise.all(roots.splice(0).map(root => rm(root, { recursive: true, force: true }))) +}) + +describe('desktop startup install classification', () => { + it('persists a new install before the desktop service creates legacy evidence', async () => { + const root = await userData() + let classificationAtServiceStart: string | undefined + initializeDesktopInstall({ + userDataPath: root, + appVersion: '0.11.0', + developmentBuild: false + }, () => { + classificationAtServiceStart = JSON.parse( + // The real DesktopService creates installation.json at this point. + readFileSync(desktopInstallStatePath(join(root, 'harness')), 'utf8') + ).classification + mkdirSync(join(root, 'desktop-service'), { recursive: true }) + writeFileSync(join(root, 'desktop-service', 'installation.json'), '{}') + }) + + expect(classificationAtServiceStart).toBe('new') + expect(JSON.parse(await readFile(desktopInstallStatePath(join(root, 'harness')), 'utf8')).classification).toBe('new') + }) + + it('keeps an existing install ineligible when the service starts', async () => { + const root = await userData() + await mkdir(join(root, 'launch-root')) + let classificationAtServiceStart: string | undefined + initializeDesktopInstall({ userDataPath: root, appVersion: '0.11.0', developmentBuild: false }, () => { + classificationAtServiceStart = readFileSync( + desktopInstallStatePath(join(root, 'harness')), 'utf8' + ) + }) + expect(JSON.parse(classificationAtServiceStart ?? '').classification).toBe('existing') + }) +})