fix(managed-tools): wait for checksum handles before promotion
Windows CI / verify (push) Successful in 3m18s

Delay managed-tool installation promotion until checksum read streams have emitted close, preventing intermittent Windows rename failures caused by open file handles.

Add a deterministic end-before-close regression test and prepare the 1.0.9 patch release across package metadata, UI version text, README, changelog, and release contracts.
This commit is contained in:
Sucukdeluxe
2026-08-12 15:18:57 +02:00
parent 2cd8d97a20
commit 5113089aba
9 changed files with 65 additions and 13 deletions
+44 -2
View File
@@ -1,16 +1,23 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
const fileSystemSpies = vi.hoisted(() => ({
readFileSync: vi.fn()
readFileSync: vi.fn(),
createReadStream: vi.fn()
}));
vi.mock('node:fs', async (importOriginal) => {
const actual = await importOriginal<typeof import('node:fs')>();
fileSystemSpies.readFileSync.mockImplementation(actual.readFileSync);
return { ...actual, readFileSync: fileSystemSpies.readFileSync };
fileSystemSpies.createReadStream.mockImplementation(actual.createReadStream);
return {
...actual,
readFileSync: fileSystemSpies.readFileSync,
createReadStream: fileSystemSpies.createReadStream
};
});
import * as crypto from 'node:crypto';
import { EventEmitter } from 'node:events';
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
@@ -380,4 +387,39 @@ describe('managed tool installer', () => {
await expect(installer.status(manifest())).resolves.toMatchObject({ state: 'corrupt', verified: false });
expect(fileSystemSpies.readFileSync).not.toHaveBeenCalled();
});
it('waits for hash streams to close before promoting a verified installation', async () => {
const createReadStream = fileSystemSpies.createReadStream.getMockImplementation();
if (!createReadStream) throw new Error('createReadStream mock is not initialized');
let openStreams = 0;
fileSystemSpies.createReadStream.mockImplementation((filePath: fs.PathLike) => {
const stream = new EventEmitter();
openStreams += 1;
queueMicrotask(() => {
stream.emit('data', fs.readFileSync(filePath));
stream.emit('end');
setImmediate(() => {
openStreams -= 1;
stream.emit('close');
});
});
return stream as fs.ReadStream;
});
const installPath = path.join(directory, 'installed');
const { installer } = createInstaller({
rename: (sourcePath, destinationPath) => {
if (sourcePath.includes('.stage-') && destinationPath === installPath && openStreams > 0) {
throw new Error('hash stream still open');
}
fs.renameSync(sourcePath, destinationPath);
}
});
try {
await expect(installer.repair(manifest())).resolves.toMatchObject({ success: true });
} finally {
fileSystemSpies.createReadStream.mockImplementation(createReadStream);
}
});
});