fix(ci): wait for asynchronous NSIS uninstaller surface cleanup
Windows CI / verify (push) Failing after 3m46s
Windows CI / verify (push) Failing after 3m46s
The installer smoke asserted a clean registry and shortcut surface immediately after the silent uninstall returned and the installation directory disappeared. NSIS uninstallers run detached through a temporary copy, so shortcut and registry removal completes after the invoked process exits, and on current runner images the immediate check raced that cleanup and failed with leftover HKCU keys and shortcuts. The smoke now polls the exact same surface assertions until a bounded deadline, keeping a genuinely dirty uninstall failing, with contract coverage for the tolerated asynchronous cleanup and the persistent failure case.
This commit is contained in:
@@ -223,6 +223,19 @@ function verifyInstalledPhase(phase) {
|
|||||||
return uninstallerPath;
|
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) {
|
async function waitForPathRemoval(targetPath, timeoutMs = 10000) {
|
||||||
const deadline = Date.now() + timeoutMs;
|
const deadline = Date.now() + timeoutMs;
|
||||||
while (fs.existsSync(targetPath)) {
|
while (fs.existsSync(targetPath)) {
|
||||||
@@ -274,7 +287,7 @@ async function main() {
|
|||||||
if (!await waitForPathRemoval(phase.installationDirectory, 30000)) {
|
if (!await waitForPathRemoval(phase.installationDirectory, 30000)) {
|
||||||
throw new Error(`Silent uninstall left the installation directory behind: ${phase.installationDirectory}`);
|
throw new Error(`Silent uninstall left the installation directory behind: ${phase.installationDirectory}`);
|
||||||
}
|
}
|
||||||
assertCleanInstallerSmokeSurface(phases);
|
await waitForInstallerSurfaceClean(phases, { keyExists: registryKeyExists });
|
||||||
results.push({
|
results.push({
|
||||||
flag: phase.flag,
|
flag: phase.flag,
|
||||||
hive: phase.hive,
|
hive: phase.hive,
|
||||||
@@ -314,5 +327,6 @@ module.exports = {
|
|||||||
assertShortcutDetails,
|
assertShortcutDetails,
|
||||||
createInstallerPhases,
|
createInstallerPhases,
|
||||||
readShortcutDetails,
|
readShortcutDetails,
|
||||||
registryKeys
|
registryKeys,
|
||||||
|
waitForInstallerSurfaceClean
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ const {
|
|||||||
assertInstallerSurfaceClean,
|
assertInstallerSurfaceClean,
|
||||||
assertPathInside,
|
assertPathInside,
|
||||||
assertShortcutDetails,
|
assertShortcutDetails,
|
||||||
|
waitForInstallerSurfaceClean,
|
||||||
createInstallerPhases,
|
createInstallerPhases,
|
||||||
readShortcutDetails,
|
readShortcutDetails,
|
||||||
registryKeys
|
registryKeys
|
||||||
@@ -222,6 +223,41 @@ test('clean surface contract includes both registry hives and both shortcut scop
|
|||||||
}), /HKLM.*Twitch VOD Manager\.lnk/i);
|
}), /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', () => {
|
test('recursive cleanup is limited to the dedicated runner temp directory', () => {
|
||||||
assert.doesNotThrow(() => assertPathInside('C:\\runner-temp\\tvm-installer-123', 'C:\\runner-temp'));
|
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);
|
assert.throws(() => assertPathInside('C:\\runner-temp', 'C:\\runner-temp'), /outside its parent/i);
|
||||||
|
|||||||
Reference in New Issue
Block a user