diff --git a/scripts/smoke-test-installer.js b/scripts/smoke-test-installer.js index 2679682..fdcd337 100644 --- a/scripts/smoke-test-installer.js +++ b/scripts/smoke-test-installer.js @@ -223,6 +223,19 @@ function verifyInstalledPhase(phase) { return uninstallerPath; } +async function waitForInstallerSurfaceClean(phases, { keyExists, pathExists = fs.existsSync, timeoutMs = 30000, pollIntervalMs = 250 }) { + const deadline = Date.now() + timeoutMs; + for (;;) { + try { + assertInstallerSurfaceClean(phases, { keyExists, pathExists }); + return; + } catch (error) { + if (Date.now() >= deadline) throw error; + await new Promise((resolve) => setTimeout(resolve, pollIntervalMs)); + } + } +} + async function waitForPathRemoval(targetPath, timeoutMs = 10000) { const deadline = Date.now() + timeoutMs; while (fs.existsSync(targetPath)) { @@ -274,7 +287,7 @@ async function main() { if (!await waitForPathRemoval(phase.installationDirectory, 30000)) { throw new Error(`Silent uninstall left the installation directory behind: ${phase.installationDirectory}`); } - assertCleanInstallerSmokeSurface(phases); + await waitForInstallerSurfaceClean(phases, { keyExists: registryKeyExists }); results.push({ flag: phase.flag, hive: phase.hive, @@ -314,5 +327,6 @@ module.exports = { assertShortcutDetails, createInstallerPhases, readShortcutDetails, - registryKeys + registryKeys, + waitForInstallerSurfaceClean }; diff --git a/scripts/smoke-test-installer.test.js b/scripts/smoke-test-installer.test.js index d59ad38..278cdb5 100644 --- a/scripts/smoke-test-installer.test.js +++ b/scripts/smoke-test-installer.test.js @@ -11,6 +11,7 @@ const { assertInstallerSurfaceClean, assertPathInside, assertShortcutDetails, + waitForInstallerSurfaceClean, createInstallerPhases, readShortcutDetails, registryKeys @@ -222,6 +223,41 @@ test('clean surface contract includes both registry hives and both shortcut scop }), /HKLM.*Twitch VOD Manager\.lnk/i); }); +test('surface wait tolerates asynchronous uninstaller cleanup before the deadline', async () => { + const phases = createInstallerPhases('C:\\smoke', { + commonDesktop: 'C:\\shared-desktop', + commonPrograms: 'C:\\shared-programs', + currentDesktop: 'C:\\user-desktop', + currentPrograms: 'C:\\user-programs' + }); + let sweeps = 0; + await waitForInstallerSurfaceClean(phases, { + keyExists: (key) => { + if (key === registryKeys('HKCU').uninstall) sweeps += 1; + return sweeps < 3; + }, + pathExists: () => sweeps < 3, + timeoutMs: 5000, + pollIntervalMs: 5 + }); + assert.ok(sweeps >= 3); +}); + +test('surface wait still fails when the uninstaller never cleans up', async () => { + const phases = createInstallerPhases('C:\\smoke', { + commonDesktop: 'C:\\shared-desktop', + commonPrograms: 'C:\\shared-programs', + currentDesktop: 'C:\\user-desktop', + currentPrograms: 'C:\\user-programs' + }); + await assert.rejects(() => waitForInstallerSurfaceClean(phases, { + keyExists: () => true, + pathExists: () => false, + timeoutMs: 60, + pollIntervalMs: 5 + }), /clean registry and shortcut surface/); +}); + test('recursive cleanup is limited to the dedicated runner temp directory', () => { assert.doesNotThrow(() => assertPathInside('C:\\runner-temp\\tvm-installer-123', 'C:\\runner-temp')); assert.throws(() => assertPathInside('C:\\runner-temp', 'C:\\runner-temp'), /outside its parent/i);