mirror of
https://github.com/nexu-io/open-design.git
synced 2026-07-03 12:27:55 +08:00
fix(e2e): seed win smoke onboarding into the live daemon data dir (#4444)
The packaged Windows smoke's onboarding seed derived its target path from the installed app's baked config, which falls back to AppData and never matches the data dir the running daemon actually reads. `tools-pack win start` rewrites `namespaceBaseRoot` to the tools-pack runtime root and hands it to the runtime via OD_PACKAGED_CONFIG_PATH, so the live daemon's RUNTIME_DATA_DIR is always under `runtimeNamespaceRoot`. The seed landed in the AppData fallback instead, so the daemon never read `onboardingCompleted`. This stayed invisible until #4389 removed the step-0 "Skip" affordance from the now-required Connect onboarding step: `ensureMainAppShell` used to click Skip to reach home, so an ineffective seed didn't matter. With Skip gone and the seed missing its target, the app sat on onboarding and the smoke failed with "packaged windows runtime did not reach main app shell". Write the seed to `<runtimeNamespaceRoot>/data/app-config.json` — the exact dir the running daemon reads (already asserted for logs at line ~400) and the same approach the macOS smoke uses. Drop the now-dead manifest-derivation helper chain.
This commit is contained in:
@@ -2,7 +2,6 @@
|
||||
|
||||
import { execFile } from 'node:child_process';
|
||||
import { mkdir, readdir, readFile, rm, stat, writeFile } from 'node:fs/promises';
|
||||
import { homedir } from 'node:os';
|
||||
import { basename, dirname, isAbsolute, join, resolve, sep } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { promisify } from 'node:util';
|
||||
@@ -295,25 +294,6 @@ type DirectInstallerResult = {
|
||||
nsisLogTail: string[];
|
||||
};
|
||||
|
||||
type InstalledPackagedConfig = {
|
||||
namespaceBaseRoot?: unknown;
|
||||
};
|
||||
|
||||
type InstalledRuntimeConfig = {
|
||||
active?: {
|
||||
entry?: {
|
||||
cwd?: unknown;
|
||||
};
|
||||
root?: unknown;
|
||||
};
|
||||
};
|
||||
|
||||
type InstalledAppPackage = {
|
||||
name?: unknown;
|
||||
productName?: unknown;
|
||||
version?: unknown;
|
||||
};
|
||||
|
||||
const shouldRunPackagedWinSmoke = process.platform === 'win32' && process.env.OD_PACKAGED_E2E_WIN === '1';
|
||||
const winDescribe = shouldRunPackagedWinSmoke ? describe : describe.skip;
|
||||
|
||||
@@ -368,7 +348,7 @@ winDescribe('packaged windows runtime smoke', () => {
|
||||
);
|
||||
}
|
||||
|
||||
await seedPackagedOnboardingComplete(install.installDir);
|
||||
await seedPackagedOnboardingComplete();
|
||||
|
||||
const startDesktop = async (step: string): Promise<WinStartResult> => {
|
||||
const nextStart = await measureSmokeStep(timings, step, async () => runToolsPackJson<WinStartResult>('start'));
|
||||
@@ -1089,88 +1069,28 @@ async function readTiming(filePath: string): Promise<TimingResult> {
|
||||
return JSON.parse(await readFile(filePath, 'utf8')) as TimingResult;
|
||||
}
|
||||
|
||||
async function seedPackagedOnboardingComplete(installDir: string): Promise<void> {
|
||||
const configPath = join(await resolveExpectedDataRoot(installDir), 'app-config.json');
|
||||
async function seedPackagedOnboardingComplete(): Promise<void> {
|
||||
// Pre-mark first-run onboarding as complete so the packaged app boots
|
||||
// straight to the home shell. Since #4389 the Connect onboarding step is
|
||||
// required and has no Skip affordance, so the only way past it on a fresh
|
||||
// install is an `onboardingCompleted: true` config the daemon reads on boot.
|
||||
//
|
||||
// Write to the SAME data dir the running daemon actually reads —
|
||||
// `<runtimeNamespaceRoot>/data` — not a path derived from the installed
|
||||
// app's baked config. `tools-pack win start` rewrites the launch config's
|
||||
// `namespaceBaseRoot` to the tools-pack runtime root (see
|
||||
// writeInstalledLaunchPackagedConfig in tools/pack/src/win/lifecycle.ts) and
|
||||
// hands it to the runtime via OD_PACKAGED_CONFIG_PATH, so the live daemon's
|
||||
// RUNTIME_DATA_DIR is always under runtimeNamespaceRoot regardless of what
|
||||
// the installer baked. Deriving the path from the installed manifest landed
|
||||
// the seed elsewhere (the AppData fallback), so the daemon never saw it and
|
||||
// the app stuck on onboarding once the Skip button was removed. This mirrors
|
||||
// the macOS smoke's seed, which already writes under runtimeNamespaceRoot.
|
||||
const configPath = join(runtimeNamespaceRoot, 'data', 'app-config.json');
|
||||
await mkdir(dirname(configPath), { recursive: true });
|
||||
await writeFile(configPath, `${JSON.stringify({ onboardingCompleted: true }, null, 2)}\n`, 'utf8');
|
||||
}
|
||||
|
||||
async function resolveExpectedDataRoot(installDir: string): Promise<string> {
|
||||
return join(await resolveExpectedNamespaceRoot(installDir), 'data');
|
||||
}
|
||||
|
||||
async function resolveExpectedNamespaceRoot(installDir: string): Promise<string> {
|
||||
const installedConfig = JSON.parse(
|
||||
await readFile(await resolveInstalledPackagedConfigPath(installDir), 'utf8'),
|
||||
) as InstalledPackagedConfig;
|
||||
const configuredNamespaceBaseRoot =
|
||||
typeof installedConfig.namespaceBaseRoot === 'string' && installedConfig.namespaceBaseRoot.length > 0
|
||||
? installedConfig.namespaceBaseRoot
|
||||
: null;
|
||||
const namespaceBaseRoot =
|
||||
configuredNamespaceBaseRoot ?? join(defaultWindowsAppDataRoot(await readInstalledAppName(installDir)), 'namespaces');
|
||||
return join(resolve(namespaceBaseRoot), namespace);
|
||||
}
|
||||
|
||||
async function readInstalledAppName(installDir: string): Promise<string> {
|
||||
const appPackage = JSON.parse(
|
||||
await readFile(
|
||||
join(await resolveInstalledPayloadRoot(installDir), 'resources', 'app', 'package.json'),
|
||||
'utf8',
|
||||
),
|
||||
) as InstalledAppPackage;
|
||||
if (typeof appPackage.productName === 'string' && appPackage.productName.length > 0) return appPackage.productName;
|
||||
if (typeof appPackage.name === 'string' && appPackage.name.length > 0) return appPackage.name;
|
||||
return 'Open Design';
|
||||
}
|
||||
|
||||
async function resolveInstalledPackagedConfigPath(installDir: string): Promise<string> {
|
||||
return join(await resolveInstalledPayloadRoot(installDir), 'resources', 'open-design-config.json');
|
||||
}
|
||||
|
||||
async function resolveInstalledPayloadRoot(installDir: string): Promise<string> {
|
||||
const runtimePath = join(installDir, 'runtime.json');
|
||||
const runtimeRaw = await readFile(runtimePath, 'utf8').catch((error: NodeJS.ErrnoException) => {
|
||||
if (error.code === 'ENOENT') return null;
|
||||
throw error;
|
||||
});
|
||||
if (runtimeRaw == null) return installDir;
|
||||
|
||||
const runtime = JSON.parse(runtimeRaw) as InstalledRuntimeConfig;
|
||||
const activeRoot = safeLauncherRelativePath(runtime.active?.root);
|
||||
const activeCwd = safeLauncherRelativePath(runtime.active?.entry?.cwd);
|
||||
if (activeRoot == null || activeCwd == null) {
|
||||
throw new Error(`installed runtime.json does not describe an active payload root: ${runtimePath}`);
|
||||
}
|
||||
|
||||
const payloadRoot = resolve(installDir, activeRoot, activeCwd);
|
||||
if (!isPathInside(payloadRoot, installDir)) {
|
||||
throw new Error(`installed runtime active payload root escapes install dir: ${payloadRoot}`);
|
||||
}
|
||||
return payloadRoot;
|
||||
}
|
||||
|
||||
function safeLauncherRelativePath(value: unknown): string | null {
|
||||
if (typeof value !== 'string' || value.length === 0 || isAbsolute(value)) return null;
|
||||
const segments = value.split(/[\\/]+/);
|
||||
if (segments.some((segment) => segment.length === 0 || segment === '.' || segment === '..')) return null;
|
||||
return join(...segments);
|
||||
}
|
||||
|
||||
function defaultWindowsAppDataRoot(appName: string): string {
|
||||
return join(process.env.APPDATA ?? join(homedir(), 'AppData', 'Roaming'), appName);
|
||||
}
|
||||
|
||||
function isPathInside(filePath: string, expectedRoot: string): boolean {
|
||||
const normalizedPath = normalizePathForComparison(resolve(filePath));
|
||||
const normalizedRoot = normalizePathForComparison(resolve(expectedRoot));
|
||||
return normalizedPath === normalizedRoot || normalizedPath.startsWith(`${normalizedRoot}${sep}`);
|
||||
}
|
||||
|
||||
function normalizePathForComparison(filePath: string): string {
|
||||
return process.platform === 'win32' ? filePath.toLowerCase() : filePath;
|
||||
}
|
||||
|
||||
function resolveFromWorkspace(filePath: string): string {
|
||||
return isAbsolute(filePath) ? filePath : resolve(workspaceRoot, filePath);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user