Files
nexu-io__open-design/e2e/specs/mac.spec.ts
PerishFire 1ff25e5b2b fix(packaged): preserve MCP runtime integrity and sidecar delivery (#8027)
* fix(daemon): keep packaged MCP discovery out of package managers

* fix(packaged): preserve MCP payload identity and require sidecar supervisor

* fix(pack): finalize runtime manifests and report entry recovery

* ci: allow isolated full Windows smoke on dispatched revisions

* test(packaged): derive recovery versions from the release channel

* test(win): read portable installer logs and retain rollback diagnostics

* test(win): distinguish portable startup reconciliation from installer sync
2026-09-14 08:13:45 +00:00

3043 lines
120 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// @vitest-environment node
import { execFile } from 'node:child_process';
import { access, chmod, mkdir, readFile, rm, stat, writeFile } from 'node:fs/promises';
import { dirname, isAbsolute, join, resolve, sep } from 'node:path';
import { fileURLToPath } from 'node:url';
import { promisify } from 'node:util';
import { afterAll, beforeAll, describe, expect, test } from 'vitest';
import { createFakeAgentRuntimes, type FakeAgentRuntime } from '@/fake-agents';
import { T } from '@/timeouts';
import {
capturePackagedFailureEvidence,
PACKAGED_FAILURE_EVIDENCE_DIR,
} from '@/vitest/packaged-failure-evidence';
import {
assertPackagedHomeFirstRunResult,
codexAppServerInvocationsCompleted,
describePackagedHomeFirstRunStall,
PACKAGED_HOME_FIRST_RUN_OUTPUT,
PACKAGED_HOME_FIRST_RUN_PROMPT,
PACKAGED_HOME_FIRST_RUN_STAGE_TIMEOUT_MS,
packagedHomeFirstRunDiagnosticsExpression,
packagedHomeFirstRunExpression,
packagedHomeFirstRunInPageAwaitMs,
packagedHomeFirstRunSnapshotExpression,
packagedHomeFirstRunStageSatisfied,
packagedHomeFirstRunSubmitExpression,
type PackagedHomeFirstRunResult,
type PackagedHomeFirstRunStage,
waitForPackagedHomeFirstRunSetup,
} from '@/vitest/packaged-home-first-run';
import { createPackagedSmokeReport } from '@/vitest/packaged-report';
import {
assertPackagedPtySmokeResult,
packagedPtySmokeExpression,
} from '@/vitest/packaged-pty-smoke';
import { releaseAppVersionArgs } from '@/vitest/packaged-release-version';
import {
applyPackagedUpdateEnv,
resolvePackagedUpdateScenario,
} from '@/vitest/packaged-update-scenario';
import { resolvePackagedSmokeNamespace } from '@/vitest/suite';
import { startToolsServeUpdaterFixture, type ToolsServeUpdaterFixture } from '@/vitest/tools-serve-updater-fixture';
import { createDesktopHarness, STORAGE_KEY, waitFor } from '../lib/desktop/desktop-test-helpers.ts';
const execFileAsync = promisify(execFile);
const e2eRoot = dirname(dirname(fileURLToPath(import.meta.url)));
const workspaceRoot = dirname(e2eRoot);
const toolsPackDir = resolveFromWorkspace(process.env.OD_PACKAGED_E2E_TOOLS_PACK_DIR ?? '.tmp/tools-pack');
const namespace = resolvePackagedSmokeNamespace('mac');
const releaseChannel = process.env.OD_PACKAGED_E2E_RELEASE_CHANNEL;
const releaseVersion = process.env.OD_PACKAGED_E2E_RELEASE_VERSION;
const updateScenario = resolvePackagedUpdateScenario({ releaseChannel, releaseVersion });
const toolsPackReleaseVersionArgs = releaseAppVersionArgs(releaseVersion);
const pnpmCommand = process.env.OD_E2E_PNPM_COMMAND ?? 'pnpm';
const screenshotPath = join(toolsPackDir, 'screenshots', `${namespace}.png`);
const smokeProfile = process.env.OD_PACKAGED_E2E_MAC_SMOKE_PROFILE ?? 'core';
const verifyCoreOnly = smokeProfile === 'core';
const updateMetadataUrl = normalizeOptionalEnv(process.env.OD_PACKAGED_E2E_MAC_UPDATE_METADATA_URL);
const updateVersion = normalizeOptionalEnv(process.env.OD_PACKAGED_E2E_MAC_UPDATE_VERSION);
const updateBuildJsonPath = normalizeOptionalEnv(process.env.OD_PACKAGED_E2E_MAC_UPDATE_BUILD_JSON_PATH);
const updateFixture = normalizeOptionalEnv(process.env.OD_PACKAGED_E2E_MAC_UPDATE_FIXTURE);
const packagedInviteDeeplink =
'opendesign://workspace/invite/continue?workspace_id=packaged-smoke-workspace&member_id=packaged-smoke-member&invite_id=packaged-smoke-invite&nonce=packaged-smoke-nonce';
const outputNamespaceRoot = join(toolsPackDir, 'out', 'mac', 'namespaces', namespace);
const runtimeNamespaceRoot = join(toolsPackDir, 'runtime', 'mac', 'namespaces', namespace);
const healthExpression = `
(async () => {
const response = await fetch('/api/health');
return {
health: await response.json(),
href: location.href,
status: response.status,
title: document.title,
};
})()
`;
const pptxArchiveInspectionSource = `
async function inspectPptxArchive(bytes, expectedText) {
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
let eocd = -1;
for (let offset = bytes.length - 22; offset >= Math.max(0, bytes.length - 65557); offset -= 1) {
if (view.getUint32(offset, true) === 0x06054b50) { eocd = offset; break; }
}
if (eocd < 0) throw new Error('PPTX end-of-central-directory record not found');
const entries = new Map();
const entryCount = view.getUint16(eocd + 10, true);
let offset = view.getUint32(eocd + 16, true);
const decoder = new TextDecoder();
for (let index = 0; index < entryCount; index += 1) {
if (view.getUint32(offset, true) !== 0x02014b50) throw new Error('invalid PPTX central-directory entry');
const nameLength = view.getUint16(offset + 28, true);
const extraLength = view.getUint16(offset + 30, true);
const commentLength = view.getUint16(offset + 32, true);
const name = decoder.decode(bytes.slice(offset + 46, offset + 46 + nameLength));
entries.set(name, {
compressedSize: view.getUint32(offset + 20, true),
localOffset: view.getUint32(offset + 42, true),
method: view.getUint16(offset + 10, true),
});
offset += 46 + nameLength + extraLength + commentLength;
}
async function readText(name) {
const entry = entries.get(name);
if (!entry) throw new Error('missing PPTX entry: ' + name);
const nameLength = view.getUint16(entry.localOffset + 26, true);
const extraLength = view.getUint16(entry.localOffset + 28, true);
const start = entry.localOffset + 30 + nameLength + extraLength;
const compressed = bytes.slice(start, start + entry.compressedSize);
if (entry.method === 0) return decoder.decode(compressed);
if (entry.method !== 8) throw new Error('unsupported PPTX compression method: ' + entry.method);
const stream = new Blob([compressed]).stream().pipeThrough(new DecompressionStream('deflate-raw'));
return decoder.decode(await new Response(stream).arrayBuffer());
}
const slideNames = Array.from(entries.keys())
.filter((name) => /^ppt\\/slides\\/slide\\d+\\.xml$/.test(name))
.sort((left, right) => left.localeCompare(right, undefined, { numeric: true }));
const slides = await Promise.all(slideNames.map(readText));
return {
hasContentTypes: entries.has('[Content_Types].xml'),
hasPresentation: entries.has('ppt/presentation.xml'),
slideCount: slideNames.length,
textMatches: expectedText.map((text, index) => slides[index]?.includes(text) === true),
};
}
`;
const upgradePersistenceProjectId = `packaged-upgrade-persistence-${Date.now().toString(36)}`;
const upgradePersistenceSeedExpression = `
(async () => {
const projectId = ${JSON.stringify(upgradePersistenceProjectId)};
const html = '<!doctype html><html><head><style>' +
'html,body{margin:0}.slide{width:1920px;height:1080px;display:flex;align-items:center;justify-content:center;font:96px sans-serif;color:white}' +
'.slide:first-child{background:#17324d}.slide:last-child{background:#8b3a2b}' +
'</style></head><body><section class="slide">Upgrade From Outer</section><section class="slide">Persistence Check</section></body></html>';
const created = await fetch('/api/projects', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: projectId, name: 'Packaged upgrade persistence' }),
});
const written = created.ok
? await fetch('/api/projects/' + encodeURIComponent(projectId) + '/files', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'deck.html', content: html }),
})
: null;
return {
createdOk: created.ok,
createdStatus: created.status,
projectId,
writtenOk: written?.ok ?? false,
writtenStatus: written?.status ?? null,
};
})()
`;
function existingProjectPptxExportExpression(projectId: string): string {
return `
(async () => {
${pptxArchiveInspectionSource}
const projectId = ${JSON.stringify(projectId)};
const exported = await fetch('/api/projects/' + encodeURIComponent(projectId) + '/export/pptx', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ fileName: 'deck.html', editable: true }),
});
const bytes = new Uint8Array(await exported.arrayBuffer());
const archive = await inspectPptxArchive(bytes, ['Upgrade From Outer', 'Persistence Check']);
return {
archive,
byteLength: bytes.length,
contentType: exported.headers.get('content-type'),
magic: String.fromCharCode(...bytes.slice(0, 2)),
projectId,
status: exported.status,
};
})()
`;
}
const updaterPopupExpression = `
(() => {
const popup = document.querySelector('[data-testid="updater-popup"]');
const button = document.querySelector('[data-testid="updater-install-button"]');
const reinstallLink = document.querySelector('[data-testid="updater-reinstall-learn-more"]');
return {
installButtonVisible: button instanceof HTMLButtonElement && !button.disabled,
reinstallLinkVisible: reinstallLink instanceof HTMLElement,
text: popup?.textContent?.trim() ?? null,
title: popup?.querySelector('h2')?.textContent?.trim() ?? null,
visible: popup instanceof HTMLElement,
};
})()
`;
const clickUpdaterInstallExpression = `
(() => {
const button = document.querySelector('[data-testid="updater-install-button"]');
if (!(button instanceof HTMLButtonElement)) return { clicked: false, reason: 'missing-install-button' };
if (button.disabled) return { clicked: false, reason: 'install-button-disabled' };
button.click();
return { clicked: true };
})()
`;
const clickUpdaterRailExpression = `
(() => {
const button = document.querySelector('[data-testid="entry-nav-updater"]');
if (!(button instanceof HTMLButtonElement)) return { clicked: false, reason: 'missing-updater-rail' };
if (button.getAttribute('aria-disabled') === 'true') return { clicked: false, reason: 'updater-rail-disabled' };
button.click();
return { clicked: true };
})()
`;
const packagedOnboardingExpression = `
(() => {
const onboardingShell = document.querySelector('.entry-shell--onboarding');
const onboardingModal = document.querySelector('.entry-onboarding-modal');
// Identity is the first gate; runtime selection follows Cloud sign-in.
const cloudSignIn = document.querySelector('.onboarding-cloud__primary');
return {
cloudSignInVisible: cloudSignIn instanceof HTMLElement,
href: location.href,
onboardingVisible: onboardingShell instanceof HTMLElement && onboardingModal instanceof HTMLElement,
text: onboardingModal?.textContent?.trim().slice(0, 2000) ?? null,
title: document.title,
};
})()
`;
type DesktopStatus = {
executablePath?: string;
pid?: number;
state?: string;
title?: string | null;
url?: string | null;
windowVisible?: boolean;
};
type MacInstallResult = {
detached: boolean;
dmgPath: string;
installedAppPath: string;
mountPoint: string;
namespace: string;
};
type MacStartResult = {
appPath: string;
executablePath: string;
logPath: string;
namespace: string;
pid: number;
source: string;
status: DesktopStatus | null;
};
type MacStopResult = {
namespace: string;
remainingPids: number[];
status: string;
};
type MacUninstallResult = {
installedAppPath: string;
namespace: string;
removed: boolean;
stop: MacStopResult;
};
type MacInspectResult = {
eval?: {
error?: string;
ok: boolean;
value?: unknown;
};
screenshot?: {
path: string;
};
status: DesktopStatus | null;
update?: {
active?: {
artifact?: {
type?: string;
};
path?: string;
version?: string;
};
artifact?: {
type?: string;
url?: string;
};
availableVersion?: string;
channel?: string;
currentVersion?: string;
downloadPath?: string;
error?: {
code: string;
message: string;
};
installResult?: {
dryRun?: boolean;
path: string;
};
reinstall?: {
installedVersion?: string;
minVersion?: string;
reason: string;
url?: string;
};
state: string;
};
launcher: LauncherSnapshot;
};
type LauncherSnapshot = {
active: LauncherPointer | null;
attempt: (LauncherPointer & { channel?: string; namespace?: string }) | null;
attemptsPath: string;
channel: string;
error?: string;
exists: boolean;
handoff: unknown | null;
handoffPath: string;
lastSuccessful: LauncherPointer | null;
namespace: string;
root: string;
runtimePath: string;
stateRoot: string;
versionRoots: string[];
versionsRoot: string;
};
type LauncherPointer = {
generation: number;
version: string;
};
type LogsResult = {
logs: Record<string, { lines: string[]; logPath: string }>;
namespace: string;
};
type HealthEvalValue = {
health: {
ok?: unknown;
service?: unknown;
version?: unknown;
};
href: string;
status: number;
title: string;
};
type PptxExportEvalValue = {
archive: {
hasContentTypes: boolean;
hasPresentation: boolean;
slideCount: number;
textMatches: boolean[];
};
byteLength: number;
contentType: string | null;
magic: string;
projectId: string;
status: number;
};
type UpgradePersistenceSeed = {
createdOk: boolean;
createdStatus: number;
projectId: string;
writtenOk: boolean;
writtenStatus: number | null;
};
type DesktopIdentityMarker = {
appPath: string;
executablePath: string;
pid: number;
version: number;
};
type PayloadRuntimeAcceptance = {
coldStart: {
health: HealthEvalValue;
identity: DesktopIdentityMarker;
launcher: LauncherSnapshot;
pptx: PptxExportEvalValue;
start: MacStartResult;
};
identity: DesktopIdentityMarker;
pptx: PptxExportEvalValue;
};
type UpdaterPopupEvalValue = {
installButtonVisible: boolean;
reinstallLinkVisible: boolean;
text: string | null;
title: string | null;
visible: boolean;
};
type UpdaterClickEvalValue = {
clicked: boolean;
reason?: string;
};
type UpdaterRecoverySummary = {
cleared: NonNullable<MacInspectResult['update']>;
downloadedBeforeClear: NonNullable<MacInspectResult['update']>;
dryRunInstall: MacInspectResult['update'] | null;
popup: UpdaterPopupEvalValue;
recovered: NonNullable<MacInspectResult['update']>;
};
type PackagedOnboardingEvalValue = {
cloudSignInVisible: boolean;
href: string;
onboardingVisible: boolean;
text: string | null;
title: string;
};
const shouldRunPackagedMacSmoke = process.platform === 'darwin' && process.env.OD_PACKAGED_E2E_MAC === '1';
const macDescribe = shouldRunPackagedMacSmoke ? describe : describe.skip;
const shouldRunPackagedMacOnboardingSmoke =
shouldRunPackagedMacSmoke && process.env.OD_PACKAGED_E2E_MAC_ONBOARDING_SMOKE === '1';
const macOnboardingDescribe = shouldRunPackagedMacOnboardingSmoke ? describe : describe.skip;
const shouldRunDesktopMacSmoke = process.platform === 'darwin' && process.env.OD_DESKTOP_SMOKE === '1';
const desktopMacDescribe = shouldRunDesktopMacSmoke ? describe : describe.skip;
macDescribe('packaged mac runtime smoke', () => {
let installedAppPath: string | null = null;
let started = false;
test('[P0] @electron-smoke cold first Home run renders assistant output without refresh or workspace-tab switching', async () => {
const fakeAgentRoot = join(toolsPackDir, 'fixtures', `home-first-run-${namespace}`);
let firstRunInstalledAppPath: string | null = null;
let firstRunStarted = false;
let firstRunDesktopLogPath: string | null = null;
let firstRunFailure: unknown = null;
let invocation: FakeAgentRuntime['invocation'];
try {
await resetPackagedRuntimeState();
const fakeAgents = await createFakeAgentRuntimes({
root: fakeAgentRoot,
runtimeIds: ['codex'],
recordInvocations: true,
});
invocation = fakeAgents.codex.invocation;
const install = await runToolsPackJson<MacInstallResult>('install');
firstRunInstalledAppPath = install.installedAppPath;
await seedPackagedHomeFirstRunConfig(fakeAgents.codex.env);
const start = await runToolsPackJson<MacStartResult>('start');
firstRunStarted = true;
firstRunDesktopLogPath = start.logPath;
expect(start.source).toBe('installed');
await waitForHealthyDesktop();
const setup = await waitForPackagedHomeFirstRunSetup(async () => {
const inspect = await runToolsPackJson<MacInspectResult>('inspect', [
'--expr',
packagedHomeFirstRunExpression(),
]);
if (inspect.eval?.ok !== true) {
throw new Error(`packaged first Home run setup failed: ${formatUnknown(inspect.eval)}`);
}
return inspect.eval.value;
});
expect(setup).toMatchObject({
inputTextBeforeSubmit: PACKAGED_HOME_FIRST_RUN_PROMPT,
submitClicked: false,
});
await waitForPackagedHomeFirstRunSubmit();
// Two stages, so a red gate can say which half broke. The first ends when
// the daemon owns a finished run row; only then is a missing message a
// rendering problem rather than an unfinished one.
const runFinished = await waitForPackagedHomeFirstRunStage('run-terminal');
expect(runFinished.terminalRunStatus).toBe('succeeded');
const firstRun = await waitForPackagedHomeFirstRunStage('assistant-output');
const { report } = await createPackagedSmokeReport('mac');
await report.json('first-run/result.json', firstRun);
// Preserve the actual default/fallback decision; do not force a transport
// or bypass OD Next admission just to make the deterministic fake pass.
expect(firstRun.runId).not.toBe('');
expect(firstRun.strategyRolloutDecision).not.toBeNull();
expect(invocation).toBeDefined();
const receipts = (await readFile(invocation!.path, 'utf8')).trim().split('\n')
.map((line) => JSON.parse(line));
expect(codexAppServerInvocationsCompleted(receipts, invocation!.nonce)).toBe(true);
expect(firstRun.submitClicked).toBe(true);
expect(firstRun.projectId).toEqual(expect.any(String));
expect(firstRun.hrefBefore).toMatch(/^(od:\/\/app\/|http:\/\/127\.0\.0\.1:\d+\/$)/);
expect(firstRun.hrefAfter).toContain(`/projects/${firstRun.projectId}`);
// With nothing injected, a cold first run must reach the daemon cleanly:
// every create attempt answers 2xx, so no attempt was ever recovered from.
// The exact attempt count is deliberately not pinned — the injected outage
// made the real count unobservable, and guessing it here would trade a
// swallowed red for a false one.
expect(firstRun.createRunRequestCount).toBeGreaterThanOrEqual(1);
expect(firstRun.createRunResponseStatuses).toHaveLength(firstRun.createRunRequestCount);
for (const status of firstRun.createRunResponseStatuses) {
expect(status).toBeGreaterThanOrEqual(200);
expect(status).toBeLessThan(300);
}
expect(firstRun.runEventRequestCount).toBeGreaterThan(0);
expect(firstRun.runEventResponseStatuses).toContain(200);
expect(firstRun.runEventsContainExpectedOutput).toBe(true);
expect(firstRun.daemonAssistantText).toContain(PACKAGED_HOME_FIRST_RUN_OUTPUT);
expect(firstRun.assistantText).toContain(PACKAGED_HOME_FIRST_RUN_OUTPUT);
expect(firstRun.workspaceTabClicksBeforeOutput).toBe(0);
expect(firstRun.navigationEntryCountAfter).toBe(firstRun.navigationEntryCountBefore);
expect(firstRun.performanceTimeOriginAfter).toBe(firstRun.performanceTimeOriginBefore);
} catch (error) {
firstRunFailure = error;
throw error;
} finally {
if (invocation) {
try {
const { report } = await createPackagedSmokeReport('mac');
await capturePackagedFailureEvidence(report, 'first-run', [
{ name: 'fixture-invocations.jsonl', read: () => readFile(invocation!.path) },
]);
} catch (error) {
console.error('failed to preserve packaged fixture receipt', error);
}
}
// Capture before uninstall: cleanup removes the installed app and the next
// case's reset deletes the runtime namespace, so evidence not copied out
// here no longer exists by the time anyone reads the report.
if (firstRunFailure != null) {
await capturePackagedHomeFirstRunFailure(firstRunFailure, firstRunDesktopLogPath);
}
if (firstRunStarted || firstRunInstalledAppPath != null) {
await runToolsPackJson<MacUninstallResult>('uninstall').catch((error: unknown) => {
console.error('failed to uninstall packaged first-Home-run app during cleanup', error);
});
}
await rm(fakeAgentRoot, { force: true, recursive: true }).catch(() => undefined);
}
// Budget: every wait above (desktop health 90s, composer readiness 45s, the
// two staged run waits 90s) must be able to expire and still leave room for
// the evidence capture in `finally`. A Vitest-level timeout aborts the case
// mid-cleanup, which is exactly how the earlier failures came back with
// `logs: {skipped: true}` and nothing else.
}, 300_000);
test('installs, starts, inspects, stops, and uninstalls the built mac artifact', async () => {
const report = await createPackagedSmokeReport('mac');
const updateEnv = captureUpdateEnv();
let payloadFixture: ToolsServeUpdaterFixture | null = null;
let recoveryFixture: ToolsServeUpdaterFixture | null = null;
let recoveryPayloadPath: string | null = null;
let logs: LogsResult | { skipped: true } = { skipped: true };
let popup: UpdaterPopupEvalValue | { skipped: true } = { skipped: true };
let updateInstall: NonNullable<MacInspectResult['update']> | { skipped: true } = { skipped: true };
let updateStatus: NonNullable<MacInspectResult['update']> | { skipped: true } = { skipped: true };
let payloadRuntime: PayloadRuntimeAcceptance | { skipped: true } = { skipped: true };
let updaterRecovery: UpdaterRecoverySummary | { skipped: true } = { skipped: true };
let upgradePersistence: UpgradePersistenceSeed | { skipped: true } = { skipped: true };
let passed = false;
try {
await resetPackagedRuntimeState();
const install = await runToolsPackJson<MacInstallResult>('install');
installedAppPath = install.installedAppPath;
expect(install.namespace).toBe(namespace);
expect(install.detached).toBe(true);
expectPathInside(install.dmgPath, join(outputNamespaceRoot, 'dmg'));
expectPathInside(install.installedAppPath, join(outputNamespaceRoot, 'install', 'Applications'));
await assertMacInviteProtocolRegistration(install.installedAppPath);
await seedPackagedOnboardingComplete();
let expectedPayloadUpdateVersion: string | null = updateVersion;
if (!verifyCoreOnly) {
if (updateMetadataUrl != null && updateMetadataUrl !== '') {
assertUpdateVersionPresent('mac', updateVersion);
applyPackagedUpdateEnv(process.env, updateScenario, updateMetadataUrl, { openDryRun: false });
} else {
assertToolsServeFixtureEnabled('mac', updateFixture);
const localPayload = await resolveLocalPayloadUpdateFixture();
expectedPayloadUpdateVersion = localPayload.targetVersion;
recoveryPayloadPath = localPayload.payloadPath;
payloadFixture = await startToolsServeUpdaterFixture({
channel: updateScenario.channel,
payloadPath: localPayload.payloadPath,
platform: 'mac',
version: localPayload.targetVersion,
workspaceRoot,
});
applyPackagedUpdateEnv(process.env, updateScenario, payloadFixture.info.metadataUrl, { openDryRun: false });
}
}
const start = await runToolsPackJson<MacStartResult>('start');
started = true;
expect(start.namespace).toBe(namespace);
expect(start.source).toBe('installed');
expect(start.appPath).toBe(install.installedAppPath);
expectPathInside(start.logPath, join(runtimeNamespaceRoot, 'logs', 'desktop'));
expect(start.pid).toBeGreaterThan(0);
// `tools-pack mac start` performs a best-effort status probe before
// returning, but GitHub's macOS runners can take longer than that probe
// window to make the packaged desktop IPC-ready. Keep validating a
// non-null immediate status when available, then use the longer health
// polling below as the authoritative startup check.
if (start.status != null) {
expect(start.status.state).toBe('running');
}
const inspect = await waitForHealthyDesktop();
expect(inspect.status?.state).toBe('running');
expect(inspect.status?.url).toMatch(/^(od:\/\/app\/|http:\/\/127\.0\.0\.1:\d+\/)/);
const value = assertHealthEvalValue(inspect.eval?.value);
expect(value.href).toMatch(/^(od:\/\/app\/|http:\/\/127\.0\.0\.1:\d+\/)/);
expect(value.status).toBe(200);
expect(value.health.ok).toBe(true);
if (updateScenario.currentVersionOverride == null) {
expect(value.health.version).toBe(updateScenario.expectedCurrentVersion);
} else {
expect(value.health.version).toEqual(expect.any(String));
}
const ptyInspect = await runToolsPackJson<MacInspectResult>('inspect', [
'--expr',
packagedPtySmokeExpression('darwin'),
]);
const pty = assertPackagedPtySmokeResult(ptyInspect.eval?.value);
expect(pty.projectCreateStatus).toBe(200);
expect(pty.projectSeedStatus).toBe(200);
expect(pty.terminalCreateStatus).toBe(200);
expect(pty.stdinStatus).toBe(200);
expect(pty.output).toContain(pty.marker);
expect(pty.exitCode, JSON.stringify(pty, null, 2)).toBe(0);
expect(pty.cleanup.terminalStatus).toBe(200);
expect(pty.cleanup.projectStatus).toBe(200);
assertLauncherPointer(inspect.launcher.active, updateScenario.expectedCurrentVersion, 0, 'initial active');
assertLauncherPointer(inspect.launcher.lastSuccessful, updateScenario.expectedCurrentVersion, 0, 'initial lastSuccessful');
const protocolHotPid = inspect.status?.pid ?? start.pid;
await invokeMacInviteDeeplink(install.installedAppPath);
const protocolHotInspect = await waitForHealthyDesktop();
expect(protocolHotInspect.status?.pid).toBe(protocolHotPid);
if (verifyCoreOnly) {
const protocolStop = await runToolsPackJson<MacStopResult>('stop');
started = false;
expect(protocolStop.status).not.toBe('partial');
expect(protocolStop.remainingPids).toEqual([]);
await invokeMacInviteDeeplink(install.installedAppPath);
started = true;
const protocolColdInspect = await waitForHealthyDesktop();
expect(protocolColdInspect.status?.state).toBe('running');
expect(protocolColdInspect.status?.pid).not.toBe(protocolHotPid);
}
if (!verifyCoreOnly) {
const updaterVersion = expectedPayloadUpdateVersion;
if (updaterVersion == null || updaterVersion.length === 0) {
throw new Error('full packaged mac payload smoke requires an update target version');
}
const persistenceInspect = await runToolsPackJson<MacInspectResult>('inspect', [
'--expr',
upgradePersistenceSeedExpression,
]);
const persistence = assertUpgradePersistenceSeed(persistenceInspect.eval?.value);
upgradePersistence = persistence;
const readyUpdate = await waitForUpdaterStatus(
(status) =>
status.update?.state === 'downloaded' &&
status.update.availableVersion === updaterVersion &&
status.update.artifact?.type === 'payload' &&
typeof status.update.downloadPath === 'string',
'ready updater prompt update downloaded',
);
expect(readyUpdate.update?.downloadPath).toEqual(expect.any(String));
expectPathInside(readyUpdate.update?.downloadPath ?? '', join(runtimeNamespaceRoot, 'updates'));
popup = await openReadyUpdaterPrompt(updaterVersion);
expect(popup.visible).toBe(true);
expect(popup.title).toEqual(expect.any(String));
expect(popup.title?.trim().length).toBeGreaterThan(0);
expect(popup.installButtonVisible).toBe(true);
expect(popup.text ?? '').toContain(updaterVersion);
expect(popup.text ?? '').not.toMatch(/installer|安装器/i);
const updateInspect = await runToolsPackJson<MacInspectResult>('inspect', ['--update-action', 'status']);
expect(updateInspect.update?.state).toBe('downloaded');
expect(updateInspect.update?.artifact?.type).toBe('payload');
expect(updateInspect.update?.channel).toBe(updateScenario.channel);
expect(updateInspect.update?.currentVersion).toBe(updateScenario.expectedCurrentVersion);
expect(updateInspect.update?.availableVersion).toBe(updaterVersion);
expectPathInside(updateInspect.update?.downloadPath ?? '', join(runtimeNamespaceRoot, 'updates'));
if (updateInspect.update == null) throw new Error('mac update status is missing');
updateStatus = updateInspect.update;
const clickInstall = await runToolsPackJson<MacInspectResult>('inspect', ['--expr', clickUpdaterInstallExpression]);
const clickValue = assertUpdaterClickEvalValue(clickInstall.eval?.value);
expect(clickValue.clicked).toBe(true);
const postUpdateInspect = await waitForHealthyDesktopVersion(updaterVersion, start.pid);
started = true;
const postUpdateHealth = assertHealthEvalValue(postUpdateInspect.eval?.value);
expect(postUpdateHealth.status).toBe(200);
expect(postUpdateHealth.health.ok).toBe(true);
expect(postUpdateHealth.health.version).toBe(updaterVersion);
const confirmedGeneration = settledLauncherGeneration(postUpdateInspect.launcher, updaterVersion);
if (confirmedGeneration == null) throw new Error('post-update launcher did not settle on the target version');
assertLauncherPointer(
postUpdateInspect.launcher.active,
updaterVersion,
confirmedGeneration,
'post-relaunch active',
);
assertLauncherPointer(
postUpdateInspect.launcher.lastSuccessful,
updaterVersion,
confirmedGeneration,
'post-relaunch lastSuccessful',
);
const terminalUpdate = await waitForUpdaterStatus(
(status) => status.update?.state === 'not-available' && status.update.currentVersion === updaterVersion,
'post-relaunch updater terminal state',
);
if (terminalUpdate.update == null) throw new Error('mac terminal update status is missing');
updateInstall = terminalUpdate.update;
const identity = await readDesktopIdentityMarker();
assertPayloadDesktopIdentity(identity, postUpdateInspect.launcher, updaterVersion);
expect(postUpdateInspect.launcher.attempt).toBeNull();
assertSettledDesktopHandoff(postUpdateInspect.launcher.handoff);
const persistedPptxExpression = existingProjectPptxExportExpression(persistence.projectId);
const pptxInspect = await runToolsPackJson<MacInspectResult>('inspect', ['--expr', persistedPptxExpression]);
const pptx = assertPptxExportEvalValue(pptxInspect.eval?.value);
expect(pptx.projectId).toBe(persistence.projectId);
const coldStop = await runToolsPackJson<MacStopResult>('stop');
started = false;
expect(coldStop.status).not.toBe('partial');
expect(coldStop.remainingPids).toEqual([]);
const coldStart = await runToolsPackJson<MacStartResult>('start');
started = true;
expect(coldStart.source).toBe('installed');
expect(coldStart.appPath).toBe(install.installedAppPath);
const coldInspect = await waitForHealthyDesktopVersion(updaterVersion, identity.pid);
const coldHealth = assertHealthEvalValue(coldInspect.eval?.value);
expect(coldHealth.status).toBe(200);
expect(coldHealth.health.ok).toBe(true);
expect(coldHealth.health.version).toBe(updaterVersion);
const coldGeneration = settledLauncherGeneration(coldInspect.launcher, updaterVersion);
if (coldGeneration == null) throw new Error('cold-start launcher did not settle on the target version');
expect(coldGeneration).toBeGreaterThanOrEqual(confirmedGeneration);
assertLauncherPointer(coldInspect.launcher.active, updaterVersion, coldGeneration, 'cold-start active');
assertLauncherPointer(
coldInspect.launcher.lastSuccessful,
updaterVersion,
coldGeneration,
'cold-start lastSuccessful',
);
expect(coldInspect.launcher.attempt).toBeNull();
assertSettledDesktopHandoff(coldInspect.launcher.handoff);
const coldIdentity = await readDesktopIdentityMarker();
assertPayloadDesktopIdentity(coldIdentity, coldInspect.launcher, updaterVersion);
expect(coldIdentity.pid).not.toBe(identity.pid);
const coldPptxInspect = await runToolsPackJson<MacInspectResult>('inspect', ['--expr', persistedPptxExpression]);
const coldPptx = assertPptxExportEvalValue(coldPptxInspect.eval?.value);
expect(coldPptx.projectId).toBe(persistence.projectId);
payloadRuntime = {
coldStart: {
health: coldHealth,
identity: coldIdentity,
launcher: coldInspect.launcher,
pptx: coldPptx,
start: coldStart,
},
identity,
pptx,
};
// Same-version reinstall + clear-cache recovery (mirrors the Windows
// lane's runSameVersionUpdaterRecoveryAcceptance): the physical outer
// is still the base install while the running payload is already at
// the target version, so only an installed-outer-aware floor can
// offer this installer reinstall. macOS has no silent DMG install to
// execute, so the installer open is asserted in dry-run mode instead
// of the Windows NSIS transaction.
if (recoveryPayloadPath != null) {
await payloadFixture?.close().catch((error: unknown) => {
console.error('failed to close payload update fixture before recovery', error);
});
payloadFixture = null;
recoveryFixture = await startToolsServeUpdaterFixture({
channel: updateScenario.channel,
controlLauncherVersionMin: updaterVersion,
controlLauncherVersionUrl: 'https://example.test/updater-recovery',
payloadPath: recoveryPayloadPath,
platform: 'mac',
version: updaterVersion,
workspaceRoot,
});
applyPackagedUpdateEnv(process.env, updateScenario, recoveryFixture.info.metadataUrl, { openDryRun: true });
const recoveryStop = await runToolsPackJson<MacStopResult>('stop');
started = false;
expect(recoveryStop.status).not.toBe('partial');
const recoveryStart = await runToolsPackJson<MacStartResult>('start');
started = true;
expect(recoveryStart.source).toBe('installed');
await waitForHealthyDesktopVersion(updaterVersion, coldIdentity.pid);
const reinstallReady = await waitForUpdaterStatus(
(inspect) =>
inspect.update?.state === 'downloaded' &&
inspect.update.artifact?.type === 'dmg' &&
inspect.update.availableVersion === updaterVersion,
'same-version reinstall downloaded',
);
if (reinstallReady.update == null) throw new Error('same-version reinstall did not return updater status');
expect(reinstallReady.update.currentVersion).toBe(updaterVersion);
expect(reinstallReady.update.reinstall).toEqual({
installedVersion: updateScenario.expectedCurrentVersion,
minVersion: updaterVersion,
reason: 'outer-below-min',
url: 'https://example.test/updater-recovery',
});
const reinstallPopup = await openReadyUpdaterPrompt(updaterVersion);
expect(reinstallPopup.visible).toBe(true);
expect(reinstallPopup.installButtonVisible).toBe(true);
expect(reinstallPopup.reinstallLinkVisible).toBe(true);
const clearedInspect = await runToolsPackJson<MacInspectResult>('inspect', ['--update-action', 'clear-cache']);
if (clearedInspect.update == null) throw new Error('clear-cache did not return updater status');
expect(clearedInspect.update.state).toBe('idle');
expect(clearedInspect.update.downloadPath).toBeUndefined();
expect(clearedInspect.update.installResult).toBeUndefined();
expect(clearedInspect.update.reinstall).toBeUndefined();
// Retained launcher versions must survive a manual clear.
expect(clearedInspect.launcher.active).toEqual(reinstallReady.launcher.active);
expect(clearedInspect.launcher.lastSuccessful).toEqual(reinstallReady.launcher.lastSuccessful);
// Recovery: an explicit re-check re-derives the reinstall offer and
// re-downloads the installer artifact from the clean slate.
await runToolsPackJson<MacInspectResult>('inspect', ['--update-action', 'check']);
const recovered = await waitForUpdaterStatus(
(inspect) =>
inspect.update?.state === 'downloaded' &&
inspect.update.artifact?.type === 'dmg' &&
inspect.update.reinstall != null,
'post-clear reinstall recovery',
);
if (recovered.update == null) throw new Error('post-clear recovery did not return updater status');
const dryRunInstall = await runToolsPackJson<MacInspectResult>('inspect', ['--update-action', 'install']);
expect(dryRunInstall.update?.installResult?.dryRun).toBe(true);
// Leave a pristine updater behind for the final stop/uninstall.
const resetInspect = await runToolsPackJson<MacInspectResult>('inspect', ['--update-action', 'clear-cache']);
expect(resetInspect.update?.state).toBe('idle');
updaterRecovery = {
cleared: clearedInspect.update,
downloadedBeforeClear: reinstallReady.update,
dryRunInstall: dryRunInstall.update ?? null,
popup: reinstallPopup,
recovered: recovered.update,
};
}
}
await mkdir(dirname(screenshotPath), { recursive: true });
const screenshot = await runToolsPackJson<MacInspectResult>('inspect', ['--path', screenshotPath]);
expect(screenshot.screenshot?.path).toBe(screenshotPath);
expect(await fileSizeBytes(screenshotPath)).toBeGreaterThan(0);
await report.saveScreenshot(screenshotPath);
if (!verifyCoreOnly) {
logs = await runToolsPackJson<LogsResult>('logs');
assertLogPathsAndContent(logs);
}
const stop = await runToolsPackJson<MacStopResult>('stop');
started = false;
expect(stop.namespace).toBe(namespace);
expect(stop.status).not.toBe('partial');
expect(stop.remainingPids).toEqual([]);
const uninstall = await runToolsPackJson<MacUninstallResult>('uninstall');
installedAppPath = null;
expect(uninstall.namespace).toBe(namespace);
expect(uninstall.installedAppPath).toBe(install.installedAppPath);
expect(uninstall.removed).toBe(true);
expect(await pathExists(install.installedAppPath)).toBe(false);
await report.saveSummary({
health: value,
install: {
detached: install.detached,
dmgPath: install.dmgPath,
installedAppPath: install.installedAppPath,
mountPoint: install.mountPoint,
},
logs: 'skipped' in logs ? logs : summarizeLogs(logs),
namespace,
payloadRuntime,
pty,
screenshot: report.screenshotRelpath,
start: {
appPath: start.appPath,
executablePath: start.executablePath,
logPath: start.logPath,
pid: start.pid,
source: start.source,
status: start.status,
},
stop,
uninstall,
update: {
popup,
status: updateStatus,
install: updateInstall,
},
updaterRecovery,
upgradePersistence,
});
passed = true;
} finally {
restoreUpdateEnv(updateEnv);
await payloadFixture?.close().catch((error: unknown) => {
console.error('failed to close payload update fixture', error);
});
await recoveryFixture?.close().catch((error: unknown) => {
console.error('failed to close updater recovery fixture', error);
});
if (!passed) {
await printPackagedLogs().catch((error: unknown) => {
console.error('failed to read packaged mac logs after failure', error);
});
}
if (started || installedAppPath != null) {
await runToolsPackJson<MacUninstallResult>('uninstall').catch((error: unknown) => {
console.error('failed to uninstall packaged mac app during cleanup', error);
});
started = false;
installedAppPath = null;
}
}
}, 360_000);
// Silent startup update acceptance: with the daemon-owned allowSilentUpdates
// preference on, a payload downloaded in a previous session must apply on
// the next cold start's first scheduler tick — install, quit, and relaunch —
// without any user-facing updater action.
const silentUpdateTest = !verifyCoreOnly && updateFixture === 'tools-serve' ? test : test.skip;
silentUpdateTest('applies a downloaded payload silently on the next cold start', async () => {
const updateEnv = captureUpdateEnv();
let payloadFixtureLocal: ToolsServeUpdaterFixture | null = null;
let cleanupStarted = false;
let cleanupInstalled = false;
try {
const localPayload = await resolveLocalPayloadUpdateFixture();
const targetVersion = localPayload.targetVersion;
await resetPackagedRuntimeState();
await runToolsPackJson<MacInstallResult>('install');
cleanupInstalled = true;
await seedPackagedOnboardingComplete();
payloadFixtureLocal = await startToolsServeUpdaterFixture({
channel: updateScenario.channel,
payloadPath: localPayload.payloadPath,
platform: 'mac',
version: targetVersion,
workspaceRoot,
});
applyPackagedUpdateEnv(process.env, updateScenario, payloadFixtureLocal.info.metadataUrl, { openDryRun: false });
const start = await runToolsPackJson<MacStartResult>('start');
cleanupStarted = true;
expect(start.source).toBe('installed');
await waitForUpdaterStatus(
(status) =>
status.update?.state === 'downloaded' &&
status.update.availableVersion === targetVersion &&
status.update.artifact?.type === 'payload',
'payload downloaded before silent restart',
);
// Enable the daemon-owned preference through the production HTTP path
// (the same GET + merged PUT the web settings surface performs).
const enableSilent = await runToolsPackJson<MacInspectResult>('inspect', ['--expr', `
(async () => {
const current = await (await fetch('/api/app-config')).json();
const response = await fetch('/api/app-config', {
headers: { 'content-type': 'application/json' },
method: 'PUT',
body: JSON.stringify({ ...(current.config ?? {}), allowSilentUpdates: true }),
});
const written = await response.json();
return { ok: response.ok, allowSilentUpdates: written.config?.allowSilentUpdates };
})()
`]);
expect(enableSilent.eval?.value).toEqual({ allowSilentUpdates: true, ok: true });
const stop = await runToolsPackJson<MacStopResult>('stop');
cleanupStarted = false;
expect(stop.status).not.toBe('partial');
// Cold start: the first scheduler tick applies the already-downloaded
// payload silently and relaunches; no updater action is issued here.
const coldStart = await runToolsPackJson<MacStartResult>('start');
cleanupStarted = true;
expect(coldStart.source).toBe('installed');
const silent = await waitForHealthyDesktopVersion(targetVersion, start.pid);
const silentHealth = assertHealthEvalValue(silent.eval?.value);
expect(silentHealth.health.version).toBe(targetVersion);
const silentGeneration = settledLauncherGeneration(silent.launcher, targetVersion);
expect(silentGeneration).not.toBeNull();
expect(silent.launcher.active?.version).toBe(targetVersion);
expect(silent.launcher.lastSuccessful?.version).toBe(targetVersion);
expect(silent.launcher.attempt).toBeNull();
const terminal = await waitForUpdaterStatus(
(status) => status.update?.state === 'not-available' && status.update.currentVersion === targetVersion,
'silent update terminal state',
);
expect(terminal.update?.currentVersion).toBe(targetVersion);
} finally {
restoreUpdateEnv(updateEnv);
await payloadFixtureLocal?.close().catch((error: unknown) => {
console.error('failed to close silent update fixture', error);
});
if (cleanupStarted) {
await runToolsPackJson<MacStopResult>('stop').catch((error: unknown) => {
console.error('failed to stop packaged mac app during silent-update cleanup', error);
});
}
if (cleanupInstalled) {
await runToolsPackJson<MacUninstallResult>('uninstall').catch((error: unknown) => {
console.error('failed to uninstall packaged mac app during silent-update cleanup', error);
});
}
}
}, 360_000);
// Crash-rollback acceptance: a payload that spawns but dies before its own
// bookkeeping must leave the pre-armed attempt behind, and the next cold
// start must roll back to the last successful version instead of retrying
// the broken payload forever. A follow-up update with a healthy payload
// then self-heals to the target version.
const rollbackTest = !verifyCoreOnly && updateFixture === 'tools-serve' ? test : test.skip;
rollbackTest('rolls back a crashing payload and self-heals on the next good update', async () => {
const updateEnv = captureUpdateEnv();
let corruptFixture: ToolsServeUpdaterFixture | null = null;
let goodFixture: ToolsServeUpdaterFixture | null = null;
const corruptWorkDir = join(toolsPackDir, 'corrupt-payload-fixture');
let cleanupStarted = false;
let cleanupInstalled = false;
try {
const localPayload = await resolveLocalPayloadUpdateFixture();
const targetVersion = localPayload.targetVersion;
const corruptPayloadPath = await buildCorruptedMacPayloadFixture(localPayload.payloadPath, corruptWorkDir);
await resetPackagedRuntimeState();
const install = await runToolsPackJson<MacInstallResult>('install');
cleanupInstalled = true;
await seedPackagedOnboardingComplete();
corruptFixture = await startToolsServeUpdaterFixture({
channel: updateScenario.channel,
payloadPath: corruptPayloadPath,
platform: 'mac',
version: targetVersion,
workspaceRoot,
});
applyPackagedUpdateEnv(process.env, updateScenario, corruptFixture.info.metadataUrl, { openDryRun: false });
const start = await runToolsPackJson<MacStartResult>('start');
cleanupStarted = true;
expect(start.source).toBe('installed');
const readyUpdate = await waitForUpdaterStatus(
(status) =>
status.update?.state === 'downloaded' &&
status.update.availableVersion === targetVersion &&
status.update.artifact?.type === 'payload',
'corrupt payload downloaded',
);
const launcherRuntimePath = readyUpdate.launcher.runtimePath;
const launcherAttemptsPath = readyUpdate.launcher.attemptsPath;
const popup = await openReadyUpdaterPrompt(targetVersion);
expect(popup.installButtonVisible).toBe(true);
const clickInstall = await runToolsPackJson<MacInspectResult>('inspect', ['--expr', clickUpdaterInstallExpression]);
expect(assertUpdaterClickEvalValue(clickInstall.eval?.value).clicked).toBe(true);
// The app quits for the relaunch; the corrupted payload stub then exits
// before any launcher bookkeeping. Wait for the desktop to disappear.
await waitForDesktopGone('crashing payload never became the desktop');
cleanupStarted = false;
// The pre-armed attempt is the rollback evidence the crash left behind.
const strandedAttempt = JSON.parse(await readFile(launcherAttemptsPath, 'utf8')) as {
generation?: number;
version?: string;
};
expect(strandedAttempt.version).toBe(targetVersion);
const strandedRuntime = JSON.parse(await readFile(launcherRuntimePath, 'utf8')) as {
active?: { generation?: number; version?: string };
lastSuccessful?: { generation?: number; version?: string };
};
expect(strandedRuntime.active?.version).toBe(targetVersion);
expect(strandedRuntime.lastSuccessful?.version).toBe(updateScenario.expectedCurrentVersion);
expect(strandedAttempt.generation).toBe(strandedRuntime.active?.generation);
// Cold start rolls back: the installed outer sees the unconfirmed
// attempt, selects lastSuccessful, and serves the base version again.
const rollbackStart = await runToolsPackJson<MacStartResult>('start');
cleanupStarted = true;
expect(rollbackStart.source).toBe('installed');
const rolledBack = await waitForHealthyDesktopVersion(updateScenario.expectedCurrentVersion, start.pid, false);
const rolledBackHealth = assertHealthEvalValue(rolledBack.eval?.value);
expect(rolledBackHealth.health.version).toBe(updateScenario.expectedCurrentVersion);
expect(rolledBack.launcher.lastSuccessful?.version).toBe(updateScenario.expectedCurrentVersion);
// Degraded steady state: the broken pointer stays active with its
// attempt as evidence until a healthy release replaces it.
expect(rolledBack.launcher.active?.version).toBe(targetVersion);
expect(rolledBack.launcher.attempt?.version).toBe(targetVersion);
// Self-heal: real recovery releases ship as version+1 (versioned
// artifacts are immutable), so the next update arrives under a bumped
// version with a healthy payload and converges.
const healedVersion = resolvePackagedUpdateScenario({
releaseChannel: updateScenario.channel,
releaseVersion: targetVersion,
}).fixtureVersion;
const healedPayloadPath = await buildVersionBumpedMacPayloadFixture(
localPayload.payloadPath,
corruptWorkDir,
healedVersion,
);
await corruptFixture.close();
corruptFixture = null;
goodFixture = await startToolsServeUpdaterFixture({
channel: updateScenario.channel,
payloadPath: healedPayloadPath,
platform: 'mac',
version: healedVersion,
workspaceRoot,
});
applyPackagedUpdateEnv(process.env, updateScenario, goodFixture.info.metadataUrl, { openDryRun: false });
const healStop = await runToolsPackJson<MacStopResult>('stop');
cleanupStarted = false;
expect(healStop.status).not.toBe('partial');
const healStart = await runToolsPackJson<MacStartResult>('start');
cleanupStarted = true;
expect(healStart.source).toBe('installed');
await waitForUpdaterStatus(
(status) =>
status.update?.state === 'downloaded' &&
status.update.availableVersion === healedVersion &&
status.update.artifact?.type === 'payload',
'healthy payload downloaded after rollback',
);
await openReadyUpdaterPrompt(healedVersion);
const healClick = await runToolsPackJson<MacInspectResult>('inspect', ['--expr', clickUpdaterInstallExpression]);
expect(assertUpdaterClickEvalValue(healClick.eval?.value).clicked).toBe(true);
const healed = await waitForHealthyDesktopVersion(healedVersion, rollbackStart.pid);
const healedHealth = assertHealthEvalValue(healed.eval?.value);
expect(healedHealth.health.version).toBe(healedVersion);
const healedGeneration = settledLauncherGeneration(healed.launcher, healedVersion);
expect(healedGeneration).not.toBeNull();
expect(healed.launcher.active?.version).toBe(healedVersion);
expect(healed.launcher.lastSuccessful?.version).toBe(healedVersion);
expect(healed.launcher.attempt).toBeNull();
} finally {
restoreUpdateEnv(updateEnv);
await corruptFixture?.close().catch((error: unknown) => {
console.error('failed to close corrupt payload fixture', error);
});
await goodFixture?.close().catch((error: unknown) => {
console.error('failed to close healthy payload fixture', error);
});
await rm(corruptWorkDir, { force: true, recursive: true }).catch(() => undefined);
if (cleanupStarted) {
await runToolsPackJson<MacStopResult>('stop').catch((error: unknown) => {
console.error('failed to stop packaged mac app during rollback cleanup', error);
});
}
if (cleanupInstalled) {
await runToolsPackJson<MacUninstallResult>('uninstall').catch((error: unknown) => {
console.error('failed to uninstall packaged mac app during rollback cleanup', error);
});
}
}
}, 360_000);
});
macOnboardingDescribe('packaged mac onboarding AMR smoke', () => {
let installedAppPath: string | null = null;
let started = false;
test('[P0] @electron-smoke starts a fresh packaged app on the Cloud identity gate', async () => {
const report = await createPackagedSmokeReport('mac');
let passed = false;
try {
await runToolsPackJson<MacUninstallResult>('uninstall').catch((error: unknown) => {
console.error('failed to uninstall stale packaged mac app before onboarding smoke', error);
});
await resetPackagedMacRuntimeData();
const install = await runToolsPackJson<MacInstallResult>('install');
installedAppPath = install.installedAppPath;
expect(install.namespace).toBe(namespace);
expect(install.detached).toBe(true);
const start = await runToolsPackJson<MacStartResult>('start');
started = true;
expect(start.namespace).toBe(namespace);
expect(start.source).toBe('installed');
expect(start.appPath).toBe(install.installedAppPath);
const inspect = await waitForHealthyDesktop();
const health = assertHealthEvalValue(inspect.eval?.value);
expect(health.status).toBe(200);
expect(health.health.ok).toBe(true);
const initial = await waitForPackagedOnboarding((snapshot) =>
snapshot.onboardingVisible && snapshot.cloudSignInVisible,
'fresh packaged onboarding Cloud identity gate',
);
expect(initial.href).toMatch(/^(od:\/\/app\/|http:\/\/127\.0\.0\.1:\d+\/)/);
expect(initial.cloudSignInVisible).toBe(true);
const onboardingScreenshotPath = join(toolsPackDir, 'screenshots', `${namespace}-onboarding.png`);
await mkdir(dirname(onboardingScreenshotPath), { recursive: true });
const screenshot = await runToolsPackJson<MacInspectResult>('inspect', ['--path', onboardingScreenshotPath]);
expect(screenshot.screenshot?.path).toBe(onboardingScreenshotPath);
expect(await fileSizeBytes(onboardingScreenshotPath)).toBeGreaterThan(0);
await report.report.save('screenshots/open-design-mac-onboarding-smoke.png', await readFile(onboardingScreenshotPath));
await report.report.json('onboarding-summary.json', {
health,
initial,
namespace,
screenshot: 'screenshots/open-design-mac-onboarding-smoke.png',
start: {
appPath: start.appPath,
executablePath: start.executablePath,
logPath: start.logPath,
pid: start.pid,
source: start.source,
status: start.status,
},
});
const stop = await runToolsPackJson<MacStopResult>('stop');
started = false;
expect(stop.namespace).toBe(namespace);
expect(stop.status).not.toBe('partial');
const uninstall = await runToolsPackJson<MacUninstallResult>('uninstall');
installedAppPath = null;
expect(uninstall.namespace).toBe(namespace);
expect(uninstall.installedAppPath).toBe(install.installedAppPath);
expect(uninstall.removed).toBe(true);
await resetPackagedMacRuntimeData();
passed = true;
} finally {
if (!passed) {
await printPackagedLogs().catch((error: unknown) => {
console.error('failed to read packaged mac onboarding logs after failure', error);
});
}
if (started || installedAppPath != null) {
await runToolsPackJson<MacUninstallResult>('uninstall').catch((error: unknown) => {
console.error('failed to uninstall packaged mac onboarding app during cleanup', error);
});
started = false;
installedAppPath = null;
}
await resetPackagedMacRuntimeData().catch((error: unknown) => {
console.error('failed to reset packaged mac onboarding runtime data during cleanup', error);
});
}
}, 180_000);
});
desktopMacDescribe('mac desktop settings smoke', () => {
const desktop = createDesktopHarness('mac-settings-smoke');
beforeAll(async () => {
await desktop.start();
}, T.xlong + T.long + T.medium);
afterAll(async () => {
await desktop.stop();
}, 30_000);
test('opens the current API configuration from the desktop shell', async () => {
await seedDesktopConfig(desktop, {
mode: 'api',
apiKey: 'sk-test',
baseUrl: 'https://api.anthropic.com',
model: 'claude-sonnet-4-5',
apiProtocol: 'anthropic',
apiProviderBaseUrl: 'https://api.anthropic.com',
agentId: null,
skillId: null,
designSystemId: null,
onboardingCompleted: true,
mediaProviders: {},
agentModels: {},
theme: 'system',
}, 'model');
await desktop.openSettings();
await openDesktopSettingsSection(desktop, 'Execution mode');
await waitFor(async () => {
const snapshot = await readDesktopSettingsSnapshot(desktop);
expect(snapshot.dialogOpen).toBe(true);
expect(snapshot.heading).toBe('Execution mode');
expect(snapshot.selectedProtocol).toBe('Anthropic API');
expect(snapshot.quickFillProvider).toBe('Anthropic (Claude)');
expect(snapshot.baseUrl).toBe('https://api.anthropic.com');
expect(snapshot.model).toBe('claude-sonnet-4-5');
});
}, 45_000);
test('keeps legacy provider tracking coherent when switching API protocols', async () => {
await seedDesktopConfig(desktop, {
mode: 'api',
apiKey: 'sk-test',
baseUrl: 'https://api.deepseek.com',
model: 'deepseek-v4-flash',
agentId: null,
skillId: null,
designSystemId: null,
onboardingCompleted: true,
mediaProviders: {},
agentModels: {},
}, 'baseUrl');
await desktop.openSettings();
await openDesktopSettingsSection(desktop, 'Execution mode');
await waitFor(async () => {
const snapshot = await readDesktopSettingsSnapshot(desktop);
expect(snapshot.dialogOpen).toBe(true);
expect(snapshot.selectedProtocol).toBe('OpenAI API');
expect(snapshot.quickFillProvider).toBe('DeepSeek — OpenAI');
expect(snapshot.baseUrl).toBe('https://api.deepseek.com');
});
await clickDesktopProtocolTab(desktop, 'Anthropic');
await waitFor(async () => {
const snapshot = await readDesktopSettingsSnapshot(desktop);
expect(snapshot.selectedProtocol).toBe('Anthropic API');
expect(snapshot.quickFillProvider).toBe('DeepSeek — Anthropic');
expect(snapshot.baseUrl).toBe('https://api.deepseek.com/anthropic');
expect(snapshot.model).toBe('deepseek-v4-flash');
});
}, 45_000);
// #5517 removed the theme segmented control from Settings, so the packaged
// "preview then save" appearance loop is now driven by the accent swatches —
// the only appearance control the section still owns. The invariants under
// test are the same ones the theme leg used to prove: the edit previews
// immediately on the live document, and it survives the dialog closing via
// Save. The seeded `theme` is a LEGACY dark value: the theme setting is gone
// and the app ships light-only, so the packaged runtime must coerce it to
// light on read rather than carry it into the document.
test('previews and saves the desktop appearance preference', async () => {
await seedDesktopConfig(desktop, {
mode: 'api',
apiKey: 'sk-test',
baseUrl: 'https://api.anthropic.com',
model: 'claude-sonnet-4-5',
apiProtocol: 'anthropic',
apiProviderBaseUrl: 'https://api.anthropic.com',
agentId: null,
skillId: null,
designSystemId: null,
onboardingCompleted: true,
mediaProviders: {},
agentModels: {},
theme: 'dark',
}, 'theme');
await desktop.openSettings();
await openDesktopSettingsSection(desktop, 'Appearance');
await clickDesktopAccentSwatch(desktop, '#87ea5c');
await waitFor(async () => {
const snapshot = await readDesktopAppearanceSnapshot(desktop);
expect(snapshot.dialogOpen).toBe(true);
// Live preview lands on the document before anything is saved.
expect(snapshot.documentAccent).toBe('#87ea5c');
// The seeded legacy `dark` never reaches the document, and the coerced
// value is written back so the dark preference stops existing on disk.
expect(snapshot.documentTheme).toBe('light');
expect(snapshot.savedTheme).toBe('light');
});
await clickDesktopSettingsFooterButton(desktop, 'primary');
await waitFor(async () => {
const snapshot = await readDesktopAppearanceSnapshot(desktop);
expect(snapshot.dialogOpen).toBe(false);
expect(snapshot.documentAccent).toBe('#87ea5c');
expect(snapshot.savedAccent).toBe('#87ea5c');
expect(snapshot.savedTheme).toBe('light');
});
}, 45_000);
test('opens Local CLI settings and exposes Codex path fields from the desktop shell', async () => {
await seedDesktopConfig(desktop, {
mode: 'daemon',
apiKey: '',
baseUrl: 'https://api.openai.com/v1',
model: 'gpt-4o',
apiProtocol: 'openai',
apiProviderBaseUrl: 'https://api.openai.com/v1',
agentId: 'codex',
skillId: null,
designSystemId: null,
onboardingCompleted: true,
mediaProviders: {},
agentModels: {},
agentCliEnv: {
codex: {
CODEX_HOME: '~/.codex-team',
CODEX_BIN: '~/bin/codex-next',
},
},
theme: 'system',
}, 'agentId');
await desktop.openSettings();
await openDesktopSettingsSection(desktop, 'Execution mode');
await clickDesktopExecutionModeTab(desktop, 'Local CLI');
await waitFor(async () => {
const snapshot = await readDesktopLocalCliSnapshot(desktop);
expect(snapshot.dialogOpen).toBe(true);
expect(snapshot.heading).toBe('Execution mode');
expect(snapshot.localCliTabSelected).toBe(true);
expect(snapshot.selectedAgent).toBe('Codex CLI');
expect(snapshot.codexHome).toBe('~/.codex-team');
expect(snapshot.codexExecutablePath).toBe('~/bin/codex-next');
});
}, 45_000);
test('switches between BYOK and Local CLI without losing the saved field previews', async () => {
await seedDesktopConfig(desktop, {
mode: 'daemon',
apiKey: 'sk-test',
baseUrl: 'https://api.deepseek.com',
model: 'deepseek-v4-flash',
apiProtocol: 'openai',
apiProviderBaseUrl: 'https://api.deepseek.com',
agentId: 'codex',
skillId: null,
designSystemId: null,
onboardingCompleted: true,
mediaProviders: {},
agentModels: {},
agentCliEnv: {
codex: {
CODEX_HOME: '~/.codex-switch',
CODEX_BIN: '~/bin/codex-switch',
},
},
theme: 'system',
}, 'baseUrl');
await desktop.openSettings();
await openDesktopSettingsSection(desktop, 'Execution mode');
await waitFor(async () => {
const snapshot = await readDesktopSettingsSnapshot(desktop);
expect(snapshot.selectedProtocol).toBe('OpenAI API');
expect(snapshot.quickFillProvider).toBe('DeepSeek — OpenAI');
expect(snapshot.baseUrl).toBe('https://api.deepseek.com');
expect(snapshot.model).toBe('deepseek-v4-flash');
});
await clickDesktopExecutionModeTab(desktop, 'Local CLI');
await waitFor(async () => {
const snapshot = await readDesktopLocalCliSnapshot(desktop);
expect(snapshot.localCliTabSelected).toBe(true);
expect(snapshot.selectedAgent).toBe('Codex CLI');
expect(snapshot.codexHome).toBe('~/.codex-switch');
expect(snapshot.codexExecutablePath).toBe('~/bin/codex-switch');
});
await clickDesktopExecutionModeTab(desktop, 'BYOK');
await waitFor(async () => {
const snapshot = await readDesktopSettingsSnapshot(desktop);
expect(snapshot.selectedProtocol).toBe('OpenAI API');
expect(snapshot.quickFillProvider).toBe('DeepSeek — OpenAI');
expect(snapshot.baseUrl).toBe('https://api.deepseek.com');
expect(snapshot.model).toBe('deepseek-v4-flash');
});
}, 45_000);
test('opens the Connectors section from the desktop shell and shows the catalog surface', async () => {
await seedDesktopConfig(desktop, {
mode: 'api',
apiKey: 'sk-test',
baseUrl: 'https://api.openai.com/v1',
model: 'gpt-4o',
apiProtocol: 'openai',
apiProviderBaseUrl: 'https://api.openai.com/v1',
agentId: null,
skillId: null,
designSystemId: null,
composio: { apiKeyConfigured: true },
onboardingCompleted: true,
mediaProviders: {},
agentModels: {},
theme: 'system',
}, 'model');
await desktop.openSettings();
await openDesktopSettingsSection(desktop, 'Connectors');
await waitFor(async () => {
const snapshot = await readDesktopConnectorsSnapshot(desktop);
expect(snapshot.dialogOpen).toBe(true);
expect(snapshot.heading).toBe('Connectors');
expect(snapshot.sectionTitle).toBe('Connectors');
expect(snapshot.apiKeyLabelVisible).toBe(true);
expect(snapshot.gateVisible || snapshot.gridVisible).toBe(true);
});
}, 45_000);
test('opens and closes a connector detail drawer from the desktop shell', async () => {
await seedDesktopConfig(desktop, {
mode: 'api',
apiKey: 'sk-test',
baseUrl: 'https://api.openai.com/v1',
model: 'gpt-4o',
apiProtocol: 'openai',
apiProviderBaseUrl: 'https://api.openai.com/v1',
agentId: null,
skillId: null,
designSystemId: null,
composio: { apiKeyConfigured: true },
onboardingCompleted: true,
mediaProviders: {},
agentModels: {},
theme: 'system',
}, 'model');
await desktop.openSettings();
await openDesktopSettingsSection(desktop, 'Connectors');
await waitFor(async () => {
const snapshot = await readDesktopConnectorsSnapshot(desktop);
expect(snapshot.gridVisible).toBe(true);
});
const opened = await desktop.eval<boolean>(`
(() => {
const card = document.querySelector('.connector-card');
if (!(card instanceof HTMLElement)) return false;
card.click();
return true;
})()
`);
expect(opened).toBe(true);
await waitFor(async () => {
const snapshot = await readDesktopConnectorsSnapshot(desktop);
expect(snapshot.drawerVisible).toBe(true);
expect(snapshot.drawerTitle).toBeTruthy();
});
const closed = await desktop.eval<boolean>(`
(() => {
const closeButton = document.querySelector('[data-testid="connector-drawer-close"]');
if (!(closeButton instanceof HTMLElement)) return false;
closeButton.click();
return true;
})()
`);
expect(closed).toBe(true);
await waitFor(async () => {
const snapshot = await readDesktopConnectorsSnapshot(desktop);
expect(snapshot.drawerVisible).toBe(false);
expect(snapshot.gridVisible).toBe(true);
});
}, 45_000);
test('[P0] keeps the desktop artifact preview loaded and stable after its file route opens', async () => {
await seedDesktopConfig(desktop, {
mode: 'api',
apiKey: 'sk-test',
baseUrl: 'https://api.openai.com/v1',
model: 'gpt-4o',
apiProtocol: 'openai',
apiProviderBaseUrl: 'https://api.openai.com/v1',
agentId: null,
skillId: null,
designSystemId: null,
onboardingCompleted: true,
mediaProviders: {},
agentModels: {},
theme: 'system',
}, 'model');
await desktop.eval<{ projectId: string }>(`
(async () => {
const projectId = 'desktop-open-smoke-' + Date.now().toString(36);
const projectResp = await fetch('/api/projects', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
id: projectId,
name: 'Desktop artifact open smoke',
}),
});
if (!projectResp.ok) {
throw new Error('failed to create project: ' + projectResp.status);
}
const fileResp = await fetch('/api/projects/' + encodeURIComponent(projectId) + '/files', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: 'desktop-open.html',
content: '<!doctype html><html><body><main><h1>Desktop Open Smoke</h1></main></body></html>',
artifactManifest: {
version: 1,
kind: 'html',
title: 'Desktop Open Smoke',
entry: 'desktop-open.html',
renderer: 'html',
exports: ['html'],
},
}),
});
if (!fileResp.ok) {
throw new Error('failed to seed project file: ' + fileResp.status);
}
window.location.assign('/projects/' + encodeURIComponent(projectId) + '/files/desktop-open.html');
return { projectId };
})()
`);
await waitFor(async () => {
const snapshot = await readDesktopArtifactPreviewSnapshot(desktop);
expect(snapshot.fileWorkspaceVisible).toBe(true);
expect(snapshot.selectedTab).toBe('desktop-open.html');
expect(snapshot.artifactPreviewVisible).toBe(true);
expect(snapshot.artifactPreviewActive).toBe(true);
expect(snapshot.artifactPreviewLoadedEpoch).toBeTruthy();
expect(snapshot.artifactPreviewLoadingVisible).toBe(false);
});
const stablePreview = await observeDesktopArtifactPreviewStability(desktop);
expect(stablePreview).toEqual({
activeThroughout: true,
loadedThroughout: true,
loadingSurfaceSeen: false,
sameFrameThroughout: true,
visibleThroughout: true,
});
}, T.xlong);
test('opens the Media providers section from the desktop shell and shows provider controls', async () => {
await seedDesktopConfig(desktop, {
mode: 'api',
apiKey: 'sk-test',
baseUrl: 'https://api.openai.com/v1',
model: 'gpt-4o',
apiProtocol: 'openai',
apiProviderBaseUrl: 'https://api.openai.com/v1',
agentId: null,
skillId: null,
designSystemId: null,
onboardingCompleted: true,
mediaProviders: {},
agentModels: {},
theme: 'system',
}, 'model');
await desktop.openSettings();
await openDesktopSettingsSection(desktop, 'Media providers');
await waitFor(async () => {
const snapshot = await readDesktopMediaSnapshot(desktop);
expect(snapshot.dialogOpen).toBe(true);
expect(snapshot.heading).toBe('Media providers');
expect(snapshot.sectionTitle).toBe('Media providers');
expect(snapshot.providerCardCount).toBeGreaterThan(0);
expect(snapshot.reloadVisible).toBe(true);
});
}, 45_000);
test('opens the About section from the desktop shell and renders version details or the offline placeholder', async () => {
await seedDesktopConfig(desktop, {
mode: 'api',
apiKey: 'sk-test',
baseUrl: 'https://api.openai.com/v1',
model: 'gpt-4o',
apiProtocol: 'openai',
apiProviderBaseUrl: 'https://api.openai.com/v1',
agentId: null,
skillId: null,
designSystemId: null,
onboardingCompleted: true,
mediaProviders: {},
agentModels: {},
theme: 'system',
}, 'model');
await desktop.openSettings();
await openDesktopSettingsSection(desktop, 'About');
await waitFor(async () => {
const snapshot = await readDesktopAboutSnapshot(desktop);
expect(snapshot.dialogOpen).toBe(true);
expect(snapshot.heading).toBe('About');
expect(snapshot.sectionTitle).toBe('About');
expect(snapshot.aboutListVisible || snapshot.versionUnavailableVisible).toBe(true);
});
}, 45_000);
// #5517 (product confirmed 2026-07-20) removed the 系统/浅色/深色 segmented
// control from Appearance; the theme now moves only through the account
// menu's 切换主题 row. The point of this test is unchanged — the packaged
// desktop shell can reach the Appearance section and render its controls —
// so it now asserts on the accent swatches, the section's surviving control,
// and guards that the theme segmented control has not come back.
test('opens the Appearance section from the desktop shell and shows the accent controls', async () => {
await seedDesktopConfig(desktop, {
mode: 'api',
apiKey: 'sk-test',
baseUrl: 'https://api.openai.com/v1',
model: 'gpt-4o',
apiProtocol: 'openai',
apiProviderBaseUrl: 'https://api.openai.com/v1',
agentId: null,
skillId: null,
designSystemId: null,
onboardingCompleted: true,
mediaProviders: {},
agentModels: {},
theme: 'system',
}, 'theme');
await desktop.openSettings();
await openDesktopSettingsSection(desktop, 'Appearance');
await waitFor(async () => {
const snapshot = await readDesktopAppearanceSectionSnapshot(desktop);
expect(snapshot.dialogOpen).toBe(true);
expect(snapshot.heading).toBe('Appearance');
expect(snapshot.sectionTitle).toBe('Appearance');
expect(snapshot.accentSwatchesVisible).toBe(true);
expect(snapshot.defaultAccentVisible).toBe(true);
expect(snapshot.themeSegControlVisible).toBe(false);
});
}, 45_000);
});
async function runToolsPackJson<T>(action: string, extraArgs: string[] = []): Promise<T> {
const args = [
'exec',
'tools-pack',
'mac',
action,
'--dir',
toolsPackDir,
'--namespace',
namespace,
...toolsPackReleaseVersionArgs,
'--json',
...extraArgs,
];
const result = await execFileAsync(pnpmCommand, args, {
cwd: workspaceRoot,
env: process.env,
maxBuffer: 20 * 1024 * 1024,
}).catch((error: unknown) => {
if (isExecError(error)) {
throw new Error(
[
`tools-pack mac ${action} failed`,
`stdout:\n${error.stdout}`,
`stderr:\n${error.stderr}`,
].join('\n'),
);
}
throw error;
});
try {
return JSON.parse(result.stdout) as T;
} catch (error) {
throw new Error(`tools-pack mac ${action} did not print JSON: ${String(error)}\n${result.stdout}`);
}
}
async function resolveLocalPayloadUpdateFixture(): Promise<{ payloadPath: string; targetVersion: string }> {
const fallbackBuildJsonPath = resolveFallbackUpdateBuildJsonPath();
if (fallbackBuildJsonPath == null) {
throw new Error(
'full packaged mac payload smoke requires update payload metadata; set OD_PACKAGED_E2E_MAC_UPDATE_METADATA_URL or provide mac-tools-pack-update-build.json next to OD_PACKAGED_E2E_BUILD_JSON_PATH',
);
}
const updateBuild = JSON.parse(stripUtf8Bom(await readFile(fallbackBuildJsonPath, 'utf8'))) as {
latestMacYmlPath?: unknown;
payloadPath?: unknown;
};
if (typeof updateBuild.payloadPath !== 'string' || updateBuild.payloadPath.length === 0) {
throw new Error(`upgrade build metadata missing payloadPath: ${fallbackBuildJsonPath}`);
}
const targetVersion =
updateVersion ??
(typeof updateBuild.latestMacYmlPath === 'string' && updateBuild.latestMacYmlPath.length > 0
? await readLatestMacYmlVersion(updateBuild.latestMacYmlPath)
: null);
if (targetVersion == null || targetVersion.length === 0) {
throw new Error(`upgrade build metadata missing version: ${fallbackBuildJsonPath}`);
}
return {
payloadPath: resolveFromWorkspace(updateBuild.payloadPath),
targetVersion,
};
}
function resolveFallbackUpdateBuildJsonPath(): string | null {
if (updateBuildJsonPath != null && updateBuildJsonPath !== '') return resolveFromWorkspace(updateBuildJsonPath);
const mainBuildJsonPath = normalizeOptionalEnv(process.env.OD_PACKAGED_E2E_BUILD_JSON_PATH);
if (mainBuildJsonPath == null || mainBuildJsonPath === '') return null;
return join(dirname(resolveFromWorkspace(mainBuildJsonPath)), 'mac-tools-pack-update-build.json');
}
function assertToolsServeFixtureEnabled(platformName: string, value: string | null): void {
if (value === 'tools-serve') return;
throw new Error(
`full packaged ${platformName} payload smoke requires explicit tools-serve fixture; set OD_PACKAGED_E2E_MAC_UPDATE_FIXTURE=tools-serve or provide OD_PACKAGED_E2E_MAC_UPDATE_METADATA_URL`,
);
}
function assertUpdateVersionPresent(platformName: string, value: string | null): asserts value is string {
if (value != null && value.length > 0) return;
throw new Error(`full packaged ${platformName} payload smoke requires an explicit update target version with external update metadata`);
}
async function readLatestMacYmlVersion(latestMacYmlPath: string): Promise<string | null> {
const latestMacYml = await readFile(resolveFromWorkspace(latestMacYmlPath), 'utf8').catch(() => null);
if (latestMacYml == null) return null;
const match = /^version:\s+"?([^\r\n"]+)"?/m.exec(stripUtf8Bom(latestMacYml));
return match?.[1] ?? null;
}
function stripUtf8Bom(value: string): string {
return value.charCodeAt(0) === 0xfeff ? value.slice(1) : value;
}
const UPDATE_ENV_KEYS = [
'OD_UPDATE_AUTO_CHECK',
'OD_UPDATE_ENABLED',
'OD_UPDATE_METADATA_URL',
'OD_UPDATE_CURRENT_VERSION',
'OD_UPDATE_OPEN_DRY_RUN',
] as const;
function captureUpdateEnv(): Partial<Record<(typeof UPDATE_ENV_KEYS)[number], string>> {
return Object.fromEntries(
UPDATE_ENV_KEYS
.map((key) => [key, process.env[key]] as const)
.filter((entry): entry is readonly [(typeof UPDATE_ENV_KEYS)[number], string] => entry[1] != null),
);
}
function restoreUpdateEnv(previous: Partial<Record<(typeof UPDATE_ENV_KEYS)[number], string>>): void {
for (const key of UPDATE_ENV_KEYS) {
if (previous[key] == null) delete process.env[key];
else process.env[key] = previous[key];
}
}
type DesktopHarness = ReturnType<typeof createDesktopHarness>;
type DesktopSettingsSnapshot = {
baseUrl: string | null;
dialogOpen: boolean;
heading: string | null;
model: string | null;
quickFillProvider: string | null;
selectedProtocol: string | null;
};
type DesktopLocalCliSnapshot = {
codexExecutablePath: string | null;
codexHome: string | null;
dialogOpen: boolean;
heading: string | null;
localCliTabSelected: boolean;
selectedAgent: string | null;
};
type DesktopAppearanceSnapshot = {
dialogOpen: boolean;
documentAccent: string | null;
documentTheme: string | null;
savedAccent: string | null;
savedTheme: string | null;
};
type DesktopConnectorsSnapshot = {
apiKeyLabelVisible: boolean;
dialogOpen: boolean;
drawerTitle: string | null;
drawerVisible: boolean;
gateVisible: boolean;
gridVisible: boolean;
heading: string | null;
sectionTitle: string | null;
};
type DesktopMediaSnapshot = {
dialogOpen: boolean;
heading: string | null;
providerCardCount: number;
reloadVisible: boolean;
sectionTitle: string | null;
};
type DesktopAboutSnapshot = {
aboutListVisible: boolean;
dialogOpen: boolean;
heading: string | null;
sectionTitle: string | null;
versionUnavailableVisible: boolean;
};
type DesktopAppearanceSectionSnapshot = {
accentSwatchesVisible: boolean;
defaultAccentVisible: boolean;
dialogOpen: boolean;
heading: string | null;
sectionTitle: string | null;
/** #5517 removed it; kept as a negative assertion so it cannot creep back. */
themeSegControlVisible: boolean;
};
type DesktopArtifactPreviewSnapshot = {
artifactPreviewActive: boolean;
artifactPreviewLoadedEpoch: string | null;
artifactPreviewLoadingVisible: boolean;
artifactPreviewVisible: boolean;
fileWorkspaceVisible: boolean;
selectedTab: string | null;
};
async function seedDesktopConfig(
desktop: DesktopHarness,
config: Record<string, unknown>,
stableField: string,
): Promise<void> {
await desktop.seedConfigAndReload(config, stableField);
}
async function openDesktopSettingsSection(
desktop: DesktopHarness,
label: string,
): Promise<void> {
const clicked = await desktop.eval<boolean>(`
(() => {
const section = Array.from(document.querySelectorAll('[role="dialog"] button'))
.find((node) => node.textContent?.includes(${JSON.stringify(label)}));
if (!(section instanceof HTMLElement)) return false;
section.click();
return true;
})()
`);
expect(clicked).toBe(true);
}
async function clickDesktopProtocolTab(
desktop: DesktopHarness,
label: 'Anthropic' | 'OpenAI',
): Promise<void> {
const clicked = await desktop.eval<boolean>(`
(() => {
const protocolTabs = Array.from(document.querySelectorAll('[role="tablist"]'))
.find((node) => node.getAttribute('aria-label') === 'API protocol');
const tab = Array.from(protocolTabs?.querySelectorAll('[role="tab"]') ?? [])
.find((node) => node.textContent?.trim() === ${JSON.stringify(label)});
if (!(tab instanceof HTMLElement)) return false;
tab.click();
return true;
})()
`);
expect(clicked).toBe(true);
}
async function clickDesktopExecutionModeTab(
desktop: DesktopHarness,
label: 'BYOK' | 'Local CLI',
): Promise<void> {
const clicked = await desktop.eval<boolean>(`
(() => {
const modeTabs = Array.from(document.querySelectorAll('[role="tablist"]'))
.find((node) => {
const labels = Array.from(node.querySelectorAll('[role="tab"]'))
.map((tab) => tab.textContent?.trim() ?? '');
return labels.some((text) => text.startsWith('BYOK')) &&
labels.some((text) => text.startsWith('Local CLI'));
});
const tab = Array.from(modeTabs?.querySelectorAll('[role="tab"]') ?? [])
.find((node) => node.textContent?.trim().startsWith(${JSON.stringify(label)}));
if (!(tab instanceof HTMLElement)) return false;
tab.click();
return true;
})()
`);
expect(clicked).toBe(true);
}
/**
* Click an accent swatch in the Settings › Appearance section.
*
* Replaces the old `clickDesktopSegmentButton` theme helper: the
* 系统/浅色/深色 segmented control is gone (#5517 hid it, and the theme setting
* was removed outright because the app ships light-only), leaving the accent
* swatches as the only appearance control Settings still owns. Swatches carry
* the hex as their aria-label (the default swatch is "Default accent color").
*/
async function clickDesktopAccentSwatch(
desktop: DesktopHarness,
label: string,
): Promise<void> {
const clicked = await desktop.eval<boolean>(`
(() => {
const swatch = document.querySelector(
'[role="dialog"] .pet-swatches [role="radio"][aria-label=' + ${JSON.stringify(JSON.stringify(label))} + ']',
);
if (!(swatch instanceof HTMLElement)) return false;
swatch.click();
return true;
})()
`);
expect(clicked).toBe(true);
}
async function clickDesktopSettingsFooterButton(
desktop: DesktopHarness,
className: 'ghost' | 'primary',
): Promise<void> {
const clicked = await desktop.eval<boolean>(`
(() => {
const footerButton = document.querySelector('.modal-foot button.${className}');
if (!(footerButton instanceof HTMLElement)) return false;
footerButton.click();
return true;
})()
`);
expect(clicked).toBe(true);
}
async function readDesktopSettingsSnapshot(
desktop: DesktopHarness,
): Promise<DesktopSettingsSnapshot> {
return await desktop.eval<DesktopSettingsSnapshot>(`
(() => {
const labelFields = Array.from(document.querySelectorAll('[role="dialog"] label.field'));
const getField = (label) => {
const field = labelFields.find((node) =>
node.querySelector('.field-label')?.textContent?.trim() === label,
);
if (!field) return null;
const control = field.querySelector('input, select, textarea');
if (!(control instanceof HTMLInputElement || control instanceof HTMLSelectElement || control instanceof HTMLTextAreaElement)) {
return null;
}
if (control instanceof HTMLSelectElement) {
return control.selectedOptions.item(0)?.textContent?.trim() ?? control.value;
}
return control.value;
};
const activeProtocol = Array.from(document.querySelectorAll('[role="tablist"][aria-label="API protocol"] [role="tab"]'))
.find((node) => node.getAttribute('aria-selected') === 'true');
const protocolText = activeProtocol?.textContent?.trim() ?? null;
return {
baseUrl: getField('Base URL'),
dialogOpen: Boolean(document.querySelector('[role="dialog"]')),
heading: document.querySelector('[role="dialog"] h2')?.textContent?.trim() ?? null,
model: getField('Model'),
quickFillProvider: getField('Quick fill provider'),
selectedProtocol: protocolText === 'OpenAI' || protocolText === 'Anthropic'
? protocolText + ' API'
: protocolText,
};
})()
`);
}
async function readDesktopAppearanceSnapshot(
desktop: DesktopHarness,
): Promise<DesktopAppearanceSnapshot> {
return await desktop.eval<DesktopAppearanceSnapshot>(`
(() => {
const raw = window.localStorage.getItem(${JSON.stringify(STORAGE_KEY)});
const config = raw ? JSON.parse(raw) : {};
return {
dialogOpen: Boolean(document.querySelector('[role="dialog"]')),
documentAccent: document.documentElement.style.getPropertyValue('--accent').trim() || null,
documentTheme: document.documentElement.getAttribute('data-theme'),
savedAccent: typeof config.accentColor === 'string' ? config.accentColor : null,
savedTheme: typeof config.theme === 'string' ? config.theme : null,
};
})()
`);
}
async function readDesktopConnectorsSnapshot(
desktop: DesktopHarness,
): Promise<DesktopConnectorsSnapshot> {
return await desktop.eval<DesktopConnectorsSnapshot>(`
(() => {
const fieldLabels = Array.from(document.querySelectorAll('[role="dialog"] .field-label'))
.map((node) => node.textContent?.trim() ?? '');
const sectionTitle = document.querySelector('.settings-section-connectors .section-head h3')
?.textContent?.trim() ?? null;
const drawerTitle = document.querySelector('[data-testid="connector-drawer"] h2')
?.textContent?.trim() ?? null;
return {
apiKeyLabelVisible: fieldLabels.includes('Composio API Key'),
dialogOpen: Boolean(document.querySelector('[role="dialog"]')),
drawerTitle,
drawerVisible: Boolean(document.querySelector('[data-testid="connector-drawer"]')),
gateVisible: Boolean(document.querySelector('[data-testid="connector-gate"]')),
gridVisible: Boolean(document.querySelector('[data-testid="connector-grid-wrap"]')),
heading: document.querySelector('[role="dialog"] h2')?.textContent?.trim() ?? null,
sectionTitle,
};
})()
`);
}
async function readDesktopMediaSnapshot(
desktop: DesktopHarness,
): Promise<DesktopMediaSnapshot> {
return await desktop.eval<DesktopMediaSnapshot>(`
(() => {
const sectionTitle = document.querySelector('.settings-section .section-head h3')
?.textContent?.trim() ?? null;
return {
dialogOpen: Boolean(document.querySelector('[role="dialog"]')),
heading: document.querySelector('[role="dialog"] h2')?.textContent?.trim() ?? null,
providerCardCount: document.querySelectorAll('.settings-provider-card').length,
reloadVisible: Boolean(Array.from(document.querySelectorAll('button'))
.find((node) => node.textContent?.trim() === 'Reload from daemon')),
sectionTitle,
};
})()
`);
}
async function readDesktopAboutSnapshot(
desktop: DesktopHarness,
): Promise<DesktopAboutSnapshot> {
return await desktop.eval<DesktopAboutSnapshot>(`
(() => {
const sectionTitle = document.querySelector('.settings-section .section-head h3')
?.textContent?.trim() ?? null;
const emptyCards = Array.from(document.querySelectorAll('.settings-section .empty-card'))
.map((node) => node.textContent?.trim() ?? '');
return {
aboutListVisible: Boolean(document.querySelector('.settings-about-list')),
dialogOpen: Boolean(document.querySelector('[role="dialog"]')),
heading: document.querySelector('[role="dialog"] h2')?.textContent?.trim() ?? null,
sectionTitle,
versionUnavailableVisible: emptyCards.includes('Version details are unavailable while the daemon is offline.'),
};
})()
`);
}
async function readDesktopAppearanceSectionSnapshot(
desktop: DesktopHarness,
): Promise<DesktopAppearanceSectionSnapshot> {
return await desktop.eval<DesktopAppearanceSectionSnapshot>(`
(() => {
const sectionTitle = document.querySelector('.settings-section .section-head h3')
?.textContent?.trim() ?? null;
const accentGroup = document.querySelector('.settings-section .pet-swatches[role="radiogroup"]');
const accentSwatches = accentGroup
? Array.from(accentGroup.querySelectorAll('[role="radio"]'))
: [];
return {
accentSwatchesVisible: accentSwatches.length > 0,
defaultAccentVisible: accentSwatches.some(
(node) => node.getAttribute('aria-label') === 'Default accent color',
),
dialogOpen: Boolean(document.querySelector('[role="dialog"]')),
heading: document.querySelector('[role="dialog"] h2')?.textContent?.trim() ?? null,
sectionTitle,
// Scoped by aria-label: the Notifications controls in the same dialog
// are seg-controls too, and they are not what #5517 removed.
themeSegControlVisible: Boolean(
document.querySelector('.seg-control[aria-label="Appearance"]'),
),
};
})()
`);
}
async function readDesktopArtifactPreviewSnapshot(
desktop: DesktopHarness,
): Promise<DesktopArtifactPreviewSnapshot> {
return await desktop.eval<DesktopArtifactPreviewSnapshot>(`
(() => {
const preview = document.querySelector('[data-testid="artifact-preview-frame"]');
const loadingSurface = document.querySelector('[data-testid="artifact-preview-first-load"]');
const elementIsVisible = (element) => {
if (!(element instanceof HTMLElement)) return false;
const style = getComputedStyle(element);
const rect = element.getBoundingClientRect();
return style.display !== 'none'
&& style.visibility !== 'hidden'
&& Number(style.opacity) !== 0
&& rect.width > 0
&& rect.height > 0;
};
const fileWorkspace = document.querySelector('[data-testid="file-workspace"]');
const activeTab = fileWorkspace?.querySelector('[role="tab"][aria-selected="true"]');
const activeTabLabel = activeTab?.querySelector('.ws-tab-label')?.textContent?.trim()
?? activeTab?.textContent?.trim()
?? null;
return {
artifactPreviewActive: preview?.getAttribute('data-od-active') === 'true',
artifactPreviewLoadedEpoch: preview instanceof HTMLIFrameElement
? preview.dataset.odLoadedPreviewEpoch ?? null
: null,
artifactPreviewLoadingVisible: elementIsVisible(loadingSurface),
artifactPreviewVisible: elementIsVisible(preview),
fileWorkspaceVisible: Boolean(fileWorkspace),
selectedTab: activeTabLabel,
};
})()
`);
}
async function observeDesktopArtifactPreviewStability(
desktop: DesktopHarness,
): Promise<{
activeThroughout: boolean;
loadedThroughout: boolean;
loadingSurfaceSeen: boolean;
sameFrameThroughout: boolean;
visibleThroughout: boolean;
}> {
return await desktop.eval(`
(async () => {
const selector = '[data-testid="artifact-preview-frame"]';
const firstFrame = document.querySelector(selector);
const visible = (element) => {
if (!(element instanceof HTMLElement)) return false;
const style = getComputedStyle(element);
const rect = element.getBoundingClientRect();
return element.isConnected
&& style.display !== 'none'
&& style.visibility !== 'hidden'
&& Number(style.opacity) !== 0
&& rect.width > 0
&& rect.height > 0;
};
let activeThroughout = true;
let loadedThroughout = true;
let loadingSurfaceSeen = false;
let sameFrameThroughout = firstFrame instanceof HTMLIFrameElement;
let visibleThroughout = visible(firstFrame);
for (let sample = 0; sample < 16; sample += 1) {
await new Promise((resolve) => window.setTimeout(resolve, 100));
const current = document.querySelector(selector);
sameFrameThroughout &&= current === firstFrame;
visibleThroughout &&= visible(current);
activeThroughout &&= current?.getAttribute('data-od-active') === 'true';
loadedThroughout &&= current instanceof HTMLIFrameElement
&& Boolean(current.dataset.odLoadedPreviewEpoch);
loadingSurfaceSeen ||= Array.from(
document.querySelectorAll('[data-testid="artifact-preview-first-load"]'),
).some(visible);
}
return {
activeThroughout,
loadedThroughout,
loadingSurfaceSeen,
sameFrameThroughout,
visibleThroughout,
};
})()
`);
}
async function readDesktopLocalCliSnapshot(
desktop: DesktopHarness,
): Promise<DesktopLocalCliSnapshot> {
return await desktop.eval<DesktopLocalCliSnapshot>(`
(() => {
const labelFields = Array.from(document.querySelectorAll('[role="dialog"] label.field'));
const getField = (label) => {
const field = labelFields.find((node) =>
node.querySelector('.field-label')?.textContent?.trim() === label,
);
if (!field) return null;
const control = field.querySelector('input');
return control instanceof HTMLInputElement ? control.value : null;
};
const localCliTab = Array.from(document.querySelectorAll('[role="tab"]'))
.find((node) => node.textContent?.trim().startsWith('Local CLI'));
const selectedAgent = Array.from(document.querySelectorAll('.agent-card.active .agent-card-name'))
.map((node) => node.textContent?.trim())
.find((value) => typeof value === 'string') ?? null;
return {
codexExecutablePath: getField('Codex executable path'),
codexHome: getField('Codex home'),
dialogOpen: Boolean(document.querySelector('[role="dialog"]')),
heading: document.querySelector('[role="dialog"] h2')?.textContent?.trim() ?? null,
localCliTabSelected: localCliTab?.getAttribute('aria-selected') === 'true',
selectedAgent,
};
})()
`);
}
async function waitForHealthyDesktop(): Promise<MacInspectResult> {
const timeoutMs = 90_000;
const startedAt = Date.now();
let lastResult: unknown = null;
while (Date.now() - startedAt < timeoutMs) {
try {
const inspect = await runToolsPackJson<MacInspectResult>('inspect', ['--expr', healthExpression]);
lastResult = inspect;
if (inspect.status?.state === 'running' && inspect.eval?.ok === true) {
const value = asHealthEvalValue(inspect.eval.value);
if (value?.status === 200 && value.health.ok === true && typeof value.health.version === 'string') {
return inspect;
}
}
} catch (error) {
lastResult = error;
}
await delay(1000);
}
throw new Error(`packaged mac runtime did not become healthy: ${formatUnknown(lastResult)}`);
}
/**
* Wait for one stage of the cold first run, polling inside the page.
*
* Each `tools-pack mac inspect` pays a full Node + tsx cold start, so a Node-side
* poll loop spends most of its budget starting processes instead of observing
* the product. Handing the wait to the page turns one process into one window of
* continuous observation, and a transient inspect failure now costs a retry
* rather than the whole wait.
*/
async function waitForPackagedHomeFirstRunStage(
stage: PackagedHomeFirstRunStage,
): Promise<PackagedHomeFirstRunResult> {
const timeoutMs = PACKAGED_HOME_FIRST_RUN_STAGE_TIMEOUT_MS[stage];
const startedAt = Date.now();
let snapshot: PackagedHomeFirstRunResult | null = null;
let lastError: unknown = null;
for (;;) {
const awaitMs = packagedHomeFirstRunInPageAwaitMs(timeoutMs - (Date.now() - startedAt));
try {
const inspect = await runToolsPackJson<MacInspectResult>('inspect', [
'--expr',
packagedHomeFirstRunSnapshotExpression({ awaitMs, stage }),
]);
if (inspect.eval?.ok === true) {
snapshot = assertPackagedHomeFirstRunResult(inspect.eval.value);
lastError = null;
if (packagedHomeFirstRunStageSatisfied(stage, snapshot)) return snapshot;
} else {
lastError = inspect.eval ?? inspect;
}
} catch (error) {
lastError = error;
}
if (Date.now() - startedAt >= timeoutMs) break;
await delay(250);
}
throw new Error(
[
`packaged first Home run stage "${stage}" timed out after ${Math.round((Date.now() - startedAt) / 1000)}s`,
describePackagedHomeFirstRunStall(stage, snapshot),
...(lastError == null ? [] : [`last inspection error: ${formatUnknown(lastError)}`]),
`snapshot: ${formatUnknown(snapshot)}`,
].join('\n'),
);
}
/**
* Persist what a failed cold first run left behind, before cleanup destroys it.
*
* The daemon's own run rows are the only observation that separates "the run is
* still going" from "the run failed"; the DOM, the conversation messages, and
* the run event stream all read as "no output" in both cases and cannot falsify
* one another. Capture runs while the desktop is still alive, then the desktop
* log last so it also covers the diagnostic requests themselves.
*/
async function capturePackagedHomeFirstRunFailure(
error: unknown,
desktopLogPath: string | null,
): Promise<void> {
try {
const { report } = await createPackagedSmokeReport('mac');
const entries = await capturePackagedFailureEvidence(report, PACKAGED_FAILURE_EVIDENCE_DIR, [
{ name: 'error.txt', read: async () => `${formatUnknown(error)}\n` },
{
name: 'runs.json',
read: async () => {
const inspect = await runToolsPackJson<MacInspectResult>('inspect', [
'--expr',
packagedHomeFirstRunDiagnosticsExpression(),
]);
if (inspect.eval?.ok !== true) {
throw new Error(`packaged diagnostics eval failed: ${formatUnknown(inspect.eval)}`);
}
return `${JSON.stringify(inspect.eval.value, null, 2)}\n`;
},
},
{
name: 'tools-pack-logs.json',
read: async () => `${JSON.stringify(await runToolsPackJson<LogsResult>('logs'), null, 2)}\n`,
},
{
name: 'desktop-latest.log',
read: async () => {
if (desktopLogPath == null) {
throw new Error('the packaged desktop never started, so no log path was reported');
}
return await readFile(desktopLogPath);
},
},
]);
console.error(
`packaged first Home run failure evidence in ${report.root}: ${JSON.stringify(entries, null, 2)}`,
);
} catch (captureError) {
console.error('failed to capture packaged first Home run failure evidence', captureError);
}
}
async function waitForPackagedHomeFirstRunSubmit(): Promise<void> {
const timeoutMs = 15_000;
const startedAt = Date.now();
let lastResult: unknown = null;
while (Date.now() - startedAt < timeoutMs) {
const inspect = await runToolsPackJson<MacInspectResult>('inspect', [
'--expr',
packagedHomeFirstRunSubmitExpression(),
]);
lastResult = inspect;
if (
inspect.eval?.ok === true
&& isRecord(inspect.eval.value)
&& inspect.eval.value.submitClicked === true
) {
return;
}
await delay(250);
}
throw new Error(
`packaged first Home run submit never became ready: ${formatUnknown(lastResult)}`,
);
}
async function waitForHealthyDesktopVersion(
expectedVersion: string,
previousPid: number | null | undefined,
// The rollback degraded steady state deliberately keeps the broken pointer
// active (with its attempt as evidence), so callers waiting on a rolled-back
// desktop must not require settled launcher pointers.
requireSettledLauncher = true,
): Promise<MacInspectResult> {
const timeoutMs = 120_000;
const startedAt = Date.now();
let lastResult: unknown = null;
while (Date.now() - startedAt < timeoutMs) {
try {
const inspect = await runToolsPackJson<MacInspectResult>('inspect', ['--expr', healthExpression]);
lastResult = inspect;
if (inspect.status?.state === 'running' && inspect.eval?.ok === true) {
const value = asHealthEvalValue(inspect.eval.value);
if (
value?.status === 200 &&
value.health.ok === true &&
value.health.version === expectedVersion &&
(previousPid == null || inspect.status.pid !== previousPid) &&
(!requireSettledLauncher || settledLauncherGeneration(inspect.launcher, expectedVersion) != null)
) {
return inspect;
}
}
} catch (error) {
lastResult = error;
}
await delay(1000);
}
throw new Error(`packaged mac runtime did not relaunch healthy on ${expectedVersion}: ${formatUnknown(lastResult)}`);
}
async function waitForPackagedOnboarding(
predicate: (value: PackagedOnboardingEvalValue) => boolean,
label: string,
timeoutMs = 90_000,
): Promise<PackagedOnboardingEvalValue> {
const startedAt = Date.now();
let lastResult: unknown = null;
while (Date.now() - startedAt < timeoutMs) {
try {
const inspect = await runToolsPackJson<MacInspectResult>('inspect', ['--expr', packagedOnboardingExpression]);
lastResult = inspect;
if (inspect.status?.state === 'running' && inspect.eval?.ok === true) {
const value = asPackagedOnboardingEvalValue(inspect.eval.value);
if (value != null && predicate(value)) return value;
}
} catch (error) {
lastResult = error;
}
await delay(1000);
}
throw new Error(`${label}: packaged onboarding timed out: ${formatUnknown(lastResult)}`);
}
async function waitForUpdaterStatus(
predicate: (inspect: MacInspectResult) => boolean,
label: string,
timeoutMs = 120_000,
): Promise<MacInspectResult> {
const startedAt = Date.now();
let lastResult: unknown = null;
while (Date.now() - startedAt < timeoutMs) {
try {
const inspect = await runToolsPackJson<MacInspectResult>('inspect', ['--update-action', 'status']);
lastResult = inspect;
if (predicate(inspect)) return inspect;
} catch (error) {
lastResult = error;
}
await delay(750);
}
throw new Error(`${label}: updater status timed out: ${formatUnknown(lastResult)}`);
}
async function repackMacPayloadFixture(
payloadZipPath: string,
workDir: string,
outputName: string,
mutate: (extractRoot: string, manifest: { entry?: { executable?: string }; version?: string }) => Promise<void>,
): Promise<string> {
const extractRoot = join(workDir, `${outputName}-extract`);
await rm(extractRoot, { force: true, recursive: true });
await mkdir(extractRoot, { recursive: true });
await execFileAsync('ditto', ['-x', '-k', payloadZipPath, extractRoot]);
const manifestPath = join(extractRoot, 'manifest.json');
const manifest = JSON.parse(await readFile(manifestPath, 'utf8')) as {
entry?: { executable?: string };
version?: string;
};
await mutate(extractRoot, manifest);
await writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, 'utf8');
const zipPath = join(workDir, `${outputName}.zip`);
await rm(zipPath, { force: true });
await execFileAsync('ditto', ['-c', '-k', '--sequesterRsrc', '--rsrc', extractRoot, zipPath]);
return zipPath;
}
/**
* Build a checksum-valid payload zip whose desktop executable dies before any
* launcher bookkeeping — the faithful shape of a broken release that passes
* every integrity gate (zip sha256, manifest validation, activation) and then
* crashes pre-main.
*/
async function buildCorruptedMacPayloadFixture(payloadZipPath: string, workDir: string): Promise<string> {
return await repackMacPayloadFixture(payloadZipPath, workDir, 'corrupt-payload', async (extractRoot, manifest) => {
const executableRelPath = manifest.entry?.executable;
if (executableRelPath == null || executableRelPath.length === 0) {
throw new Error(`payload manifest has no entry.executable: ${payloadZipPath}`);
}
const executablePath = join(extractRoot, executableRelPath);
await writeFile(executablePath, '#!/bin/sh\nexit 87\n', 'utf8');
await chmod(executablePath, 0o755);
});
}
/**
* Re-version a healthy payload zip to the next counted release. Real recovery
* releases ship as version+1 (versioned artifacts are immutable), so the
* self-heal update must arrive under a bumped version rather than overwriting
* the broken pointer's version root. The desktop binary is unchanged — the
* running version is config/manifest-driven.
*/
async function buildVersionBumpedMacPayloadFixture(
payloadZipPath: string,
workDir: string,
bumpedVersion: string,
): Promise<string> {
return await repackMacPayloadFixture(payloadZipPath, workDir, 'healed-payload', async (extractRoot, manifest) => {
manifest.version = bumpedVersion;
const executableRelPath = manifest.entry?.executable;
if (executableRelPath == null || executableRelPath.length === 0) {
throw new Error(`payload manifest has no entry.executable: ${payloadZipPath}`);
}
// <bundle>.app/Contents/MacOS/<binary> → <bundle>.app/Contents/Resources
const configPath = join(extractRoot, dirname(dirname(executableRelPath)), 'Resources', 'open-design-config.json');
const config = JSON.parse(await readFile(configPath, 'utf8')) as { appVersion?: string };
config.appVersion = bumpedVersion;
await writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, 'utf8');
});
}
/**
* Reset the namespace to a pristine pre-install state. `uninstall` removes the
* installed app but deliberately keeps runtime data; lifecycle tests must not
* inherit the previous test's (or a previous local run's) launcher pointers,
* update store, or daemon preferences, so each test starts from zero.
*/
async function resetPackagedRuntimeState(): Promise<void> {
await runToolsPackJson<MacStopResult>('stop').catch(() => undefined);
await runToolsPackJson<MacUninstallResult>('uninstall').catch(() => undefined);
await rm(runtimeNamespaceRoot, { force: true, recursive: true }).catch(() => undefined);
await rm(
join(toolsPackDir, 'runtime', 'mac', 'launcher', 'channels', updateScenario.channel, 'namespaces', namespace),
{ force: true, recursive: true },
).catch(() => undefined);
}
async function waitForDesktopGone(label: string, timeoutMs = 120_000): Promise<void> {
const startedAt = Date.now();
let lastResult: unknown = null;
while (Date.now() - startedAt < timeoutMs) {
try {
const inspect = await runToolsPackJson<MacInspectResult>('inspect');
lastResult = inspect;
if (inspect.status == null || inspect.status.state !== 'running') return;
} catch {
// A dead desktop IPC socket is exactly the expected terminal state.
return;
}
await delay(1000);
}
throw new Error(`${label}: desktop still running: ${formatUnknown(lastResult)}`);
}
async function openReadyUpdaterPrompt(version: string): Promise<UpdaterPopupEvalValue> {
await clickUpdaterRailButton('open ready updater prompt');
return await waitForUpdaterPopupMatching(
(popup) => popup.visible && popup.installButtonVisible && (popup.text ?? '').includes(version),
'ready updater prompt',
);
}
async function clickUpdaterRailButton(label: string, timeoutMs = 90_000): Promise<void> {
const startedAt = Date.now();
let lastResult: unknown = null;
while (Date.now() - startedAt < timeoutMs) {
try {
const click = await runToolsPackJson<MacInspectResult>('inspect', ['--expr', clickUpdaterRailExpression]);
const value = assertUpdaterClickEvalValue(click.eval?.value);
lastResult = value;
if (value.clicked) return;
} catch (error) {
lastResult = error;
}
await delay(750);
}
throw new Error(`${label}: updater rail did not become clickable: ${formatUnknown(lastResult)}`);
}
async function waitForUpdaterPopupMatching(
predicate: (value: UpdaterPopupEvalValue) => boolean,
label: string,
timeoutMs = 90_000,
): Promise<UpdaterPopupEvalValue> {
const startedAt = Date.now();
let lastResult: unknown = null;
while (Date.now() - startedAt < timeoutMs) {
try {
const inspect = await runToolsPackJson<MacInspectResult>('inspect', ['--expr', updaterPopupExpression]);
lastResult = inspect;
if (inspect.status?.state === 'running' && inspect.eval?.ok === true) {
const value = asUpdaterPopupEvalValue(inspect.eval.value);
if (value != null && predicate(value)) return value;
}
} catch (error) {
lastResult = error;
}
await delay(1000);
}
throw new Error(`${label}: updater popup timed out: ${formatUnknown(lastResult)}`);
}
async function readDesktopIdentityMarker(): Promise<DesktopIdentityMarker> {
const status = (await runToolsPackJson<MacInspectResult>('inspect')).status;
if (typeof status?.executablePath !== 'string' || typeof status.pid !== 'number') {
throw new Error(`invalid packaged desktop sidecar status: ${formatUnknown(status)}`);
}
const marker = '/Contents/MacOS/';
const markerIndex = status.executablePath.indexOf(marker);
const appPath = markerIndex < 0 ? status.executablePath : status.executablePath.slice(0, markerIndex);
return { appPath, executablePath: status.executablePath, pid: status.pid, version: 1 };
}
function assertPayloadDesktopIdentity(
identity: DesktopIdentityMarker,
launcher: LauncherSnapshot,
version: string,
): void {
const payloadRoot = join(launcher.versionsRoot, version, 'payload');
expect(identity.pid).toBeGreaterThan(0);
expectPathInside(identity.appPath, payloadRoot);
expectPathInside(identity.executablePath, payloadRoot);
}
function assertPptxExportEvalValue(value: unknown): PptxExportEvalValue {
if (
!isRecord(value) ||
!isRecord(value.archive) ||
typeof value.archive.hasContentTypes !== 'boolean' ||
typeof value.archive.hasPresentation !== 'boolean' ||
typeof value.archive.slideCount !== 'number' ||
!Array.isArray(value.archive.textMatches) ||
typeof value.byteLength !== 'number' ||
(value.contentType != null && typeof value.contentType !== 'string') ||
typeof value.magic !== 'string' ||
typeof value.projectId !== 'string' ||
typeof value.status !== 'number'
) {
throw new Error(`unexpected PPTX export eval value: ${formatUnknown(value)}`);
}
expect(value.status).toBe(200);
expect(value.contentType).toContain(
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
);
expect(value.byteLength).toBeGreaterThan(0);
expect(value.magic).toBe('PK');
expect(value.archive).toEqual({
hasContentTypes: true,
hasPresentation: true,
slideCount: 2,
textMatches: [true, true],
});
return value as PptxExportEvalValue;
}
function assertUpgradePersistenceSeed(value: unknown): UpgradePersistenceSeed {
if (
!isRecord(value) ||
typeof value.createdOk !== 'boolean' ||
typeof value.createdStatus !== 'number' ||
typeof value.projectId !== 'string' ||
typeof value.writtenOk !== 'boolean' ||
(value.writtenStatus != null && typeof value.writtenStatus !== 'number')
) {
throw new Error(`unexpected upgrade persistence seed value: ${formatUnknown(value)}`);
}
expect(value.createdOk).toBe(true);
expect(value.writtenOk).toBe(true);
return value as UpgradePersistenceSeed;
}
function assertSettledDesktopHandoff(value: unknown | null): void {
if (value == null) return;
if (!isRecord(value)) throw new Error(`invalid launcher desktop handoff: ${formatUnknown(value)}`);
expect(value.state).toBe('confirmed');
}
function assertLauncherPointer(
pointer: LauncherPointer | null,
expectedVersion: string,
expectedGeneration: number,
label: string,
): void {
expect(pointer, `${label} pointer`).toEqual({
generation: expectedGeneration,
version: expectedVersion,
});
}
function settledLauncherGeneration(launcher: LauncherSnapshot, expectedVersion: string): number | null {
const active = launcher.active;
const lastSuccessful = launcher.lastSuccessful;
if (
active == null ||
lastSuccessful == null ||
active.version !== expectedVersion ||
lastSuccessful.version !== expectedVersion ||
active.generation !== lastSuccessful.generation ||
launcher.attempt != null
) {
return null;
}
if (launcher.handoff != null && (!isRecord(launcher.handoff) || launcher.handoff.state !== 'confirmed')) {
return null;
}
return active.generation;
}
function assertLogPathsAndContent(result: LogsResult): void {
expect(result.namespace).toBe(namespace);
for (const app of ['desktop', 'web', 'daemon']) {
const entry = result.logs[app];
if (entry == null) {
throw new Error(`expected ${app} log entry`);
}
expectPathInside(entry.logPath, join(runtimeNamespaceRoot, 'logs', app));
}
const combined = Object.values(result.logs)
.flatMap((entry) => entry.lines)
.join('\n');
expect(combined).not.toMatch(/ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING/);
expect(combined).not.toMatch(/packaged runtime failed/i);
}
function summarizeLogs(result: LogsResult): Record<string, { lineCount: number; logPath: string }> {
return Object.fromEntries(
Object.entries(result.logs).map(([app, entry]) => [
app,
{
lineCount: entry.lines.length,
logPath: entry.logPath,
},
]),
);
}
async function printPackagedLogs(): Promise<void> {
const result = await runToolsPackJson<LogsResult>('logs');
for (const [app, entry] of Object.entries(result.logs)) {
console.error(`[${app}] ${entry.logPath}`);
console.error(entry.lines.join('\n') || '(no log lines)');
}
}
function assertHealthEvalValue(value: unknown): HealthEvalValue {
const normalized = asHealthEvalValue(value);
if (normalized == null) {
throw new Error(`unexpected health eval value: ${formatUnknown(value)}`);
}
return normalized;
}
function assertUpdaterClickEvalValue(value: unknown): UpdaterClickEvalValue {
const normalized = asUpdaterClickEvalValue(value);
if (normalized == null) {
throw new Error(`unexpected updater click eval value: ${formatUnknown(value)}`);
}
return normalized;
}
function asHealthEvalValue(value: unknown): HealthEvalValue | null {
if (!isRecord(value)) return null;
if (typeof value.href !== 'string' || typeof value.status !== 'number' || typeof value.title !== 'string') return null;
if (!isRecord(value.health)) return null;
return value as HealthEvalValue;
}
function asPackagedOnboardingEvalValue(value: unknown): PackagedOnboardingEvalValue | null {
if (!isRecord(value)) return null;
if (typeof value.cloudSignInVisible !== 'boolean') return null;
if (typeof value.href !== 'string') return null;
if (typeof value.onboardingVisible !== 'boolean') return null;
if (value.text != null && typeof value.text !== 'string') return null;
if (typeof value.title !== 'string') return null;
return value as PackagedOnboardingEvalValue;
}
function asUpdaterPopupEvalValue(value: unknown): UpdaterPopupEvalValue | null {
if (!isRecord(value)) return null;
if (typeof value.visible !== 'boolean') return null;
if (typeof value.installButtonVisible !== 'boolean') return null;
if (typeof value.reinstallLinkVisible !== 'boolean') return null;
if (value.title != null && typeof value.title !== 'string') return null;
if (value.text != null && typeof value.text !== 'string') return null;
return value as UpdaterPopupEvalValue;
}
function asUpdaterClickEvalValue(value: unknown): UpdaterClickEvalValue | null {
if (!isRecord(value)) return null;
if (typeof value.clicked !== 'boolean') return null;
if (value.reason != null && typeof value.reason !== 'string') return null;
return value as UpdaterClickEvalValue;
}
function expectPathInside(filePath: string, expectedRoot: string): void {
const normalizedPath = resolve(filePath);
const normalizedRoot = resolve(expectedRoot);
expect(
normalizedPath === normalizedRoot || normalizedPath.startsWith(`${normalizedRoot}${sep}`),
`${normalizedPath} should be inside ${normalizedRoot}`,
).toBe(true);
}
async function assertMacInviteProtocolRegistration(installedAppPath: string): Promise<void> {
const plistPath = join(installedAppPath, 'Contents', 'Info.plist');
const { stdout } = await execFileAsync('/usr/bin/plutil', [
'-convert',
'json',
'-o',
'-',
plistPath,
]);
const plist = JSON.parse(stdout) as {
CFBundleURLTypes?: Array<{ CFBundleURLSchemes?: string[] }>;
};
const schemes = (plist.CFBundleURLTypes ?? []).flatMap(
(entry) => entry.CFBundleURLSchemes ?? [],
);
expect(schemes).toContain('opendesign');
}
async function invokeMacInviteDeeplink(installedAppPath: string): Promise<void> {
// `-a` pins delivery to this namespace's installed test bundle instead of a
// developer's stable OpenDesign app that may own the same global scheme.
await execFileAsync('/usr/bin/open', ['-a', installedAppPath, packagedInviteDeeplink]);
}
async function pathExists(filePath: string): Promise<boolean> {
try {
await access(filePath);
return true;
} catch {
return false;
}
}
async function fileSizeBytes(filePath: string): Promise<number> {
return (await stat(filePath)).size;
}
async function seedPackagedOnboardingComplete(): Promise<void> {
// Updater flows need the ordinary signed-out Home shell. Completion alone
// is insufficient when the daemon default selects the AMR cloud agent: the
// product correctly routes that signed-out identity back to Connect even
// though first-run onboarding was completed. Pin a local agent so this
// fixture models the actual post-onboarding state it claims to create.
await seedPackagedAppConfig({ agentId: 'codex', onboardingCompleted: true });
}
async function seedPackagedHomeFirstRunConfig(
codexEnv: Record<string, string>,
): Promise<void> {
await seedPackagedAppConfig({
mode: 'daemon',
apiKey: '',
baseUrl: 'https://api.anthropic.com',
model: 'claude-sonnet-4-5',
agentId: 'codex',
skillId: null,
designSystemId: null,
onboardingCompleted: true,
mediaProviders: {},
agentModels: { codex: { model: 'default', reasoning: 'default' } },
agentCliEnv: { codex: codexEnv },
});
}
async function seedPackagedAppConfig(config: Record<string, unknown>): Promise<void> {
const configPath = join(runtimeNamespaceRoot, 'data', 'app-config.json');
await mkdir(dirname(configPath), { recursive: true });
await writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, 'utf8');
}
async function resetPackagedMacRuntimeData(): Promise<void> {
await rm(runtimeNamespaceRoot, { force: true, recursive: true });
}
function resolveFromWorkspace(filePath: string): string {
return isAbsolute(filePath) ? filePath : resolve(workspaceRoot, filePath);
}
function normalizeOptionalEnv(value: string | undefined): string | null {
const normalized = value?.trim();
return normalized == null || normalized.length === 0 ? null : normalized;
}
function delay(ms: number): Promise<void> {
return new Promise((resolveDelay) => setTimeout(resolveDelay, ms));
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value != null && !Array.isArray(value);
}
function isExecError(value: unknown): value is { stderr: string; stdout: string } {
return isRecord(value) && typeof value.stdout === 'string' && typeof value.stderr === 'string';
}
function formatUnknown(value: unknown): string {
if (value instanceof Error) return `${value.name}: ${value.message}`;
try {
return JSON.stringify(value, null, 2);
} catch {
return String(value);
}
}