mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 12:15:57 +00:00
fix(update): add line-buffering and unit tests for stderr filter
Address review feedback: - Add line-buffering to handle chunk splitting edge case - Add 14 unit tests for stderr filter logic - Test chunk boundary handling, mixed output, edge cases Handles case where "npm warn cleanup" could be split across chunks.
This commit is contained in:
@@ -218,12 +218,24 @@ async function performNpmUpdate(
|
||||
|
||||
// On Windows, filter stderr to hide npm cleanup warnings (EPERM on bcrypt.node etc.)
|
||||
// These warnings are cosmetic - update succeeds despite file locking by antivirus/indexing
|
||||
// Use line-buffering to handle chunk splitting (data events don't guarantee message boundaries)
|
||||
if (isWindows && child.stderr) {
|
||||
let stderrBuffer = '';
|
||||
child.stderr.on('data', (data: Buffer) => {
|
||||
const output = data.toString();
|
||||
// Skip npm cleanup warnings (EPERM, ENOTEMPTY, EBUSY on native module prebuilds)
|
||||
if (!/npm warn cleanup/i.test(output)) {
|
||||
process.stderr.write(data);
|
||||
stderrBuffer += data.toString();
|
||||
const lines = stderrBuffer.split('\n');
|
||||
stderrBuffer = lines.pop() || ''; // Keep incomplete line in buffer
|
||||
for (const line of lines) {
|
||||
// Skip npm cleanup warnings (EPERM, ENOTEMPTY, EBUSY on native module prebuilds)
|
||||
if (!/npm warn cleanup/i.test(line)) {
|
||||
process.stderr.write(line + '\n');
|
||||
}
|
||||
}
|
||||
});
|
||||
child.stderr.on('close', () => {
|
||||
// Flush remaining buffer on stream close
|
||||
if (stderrBuffer && !/npm warn cleanup/i.test(stderrBuffer)) {
|
||||
process.stderr.write(stderrBuffer);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,212 @@
|
||||
/**
|
||||
* Unit Tests for Update Command stderr filtering on Windows
|
||||
*
|
||||
* Tests the npm cleanup warning filter logic that hides cosmetic EPERM
|
||||
* warnings on Windows during `ccs update`. These warnings occur when
|
||||
* npm fails to unlink native module prebuilds (bcrypt.node) due to
|
||||
* antivirus/indexing file locking.
|
||||
*
|
||||
* @see https://github.com/kaitranntt/ccs/issues/405
|
||||
*/
|
||||
|
||||
const assert = require('assert');
|
||||
|
||||
/**
|
||||
* Simulates the line-buffered stderr filtering logic from update-command.ts
|
||||
* This is extracted for testability since mocking spawn is complex.
|
||||
*/
|
||||
function createStderrFilter() {
|
||||
let buffer = '';
|
||||
const output = [];
|
||||
|
||||
return {
|
||||
/**
|
||||
* Process a chunk of stderr data (simulates 'data' event)
|
||||
*/
|
||||
processChunk(chunk) {
|
||||
buffer += chunk;
|
||||
const lines = buffer.split('\n');
|
||||
buffer = lines.pop() || '';
|
||||
for (const line of lines) {
|
||||
if (!/npm warn cleanup/i.test(line)) {
|
||||
output.push(line);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Flush remaining buffer (simulates 'close' event)
|
||||
*/
|
||||
flush() {
|
||||
if (buffer && !/npm warn cleanup/i.test(buffer)) {
|
||||
output.push(buffer);
|
||||
}
|
||||
buffer = '';
|
||||
},
|
||||
|
||||
/**
|
||||
* Get filtered output lines
|
||||
*/
|
||||
getOutput() {
|
||||
return output;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get remaining buffer (for testing)
|
||||
*/
|
||||
getBuffer() {
|
||||
return buffer;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe('Update Command stderr filter', function () {
|
||||
describe('npm cleanup warning filtering', function () {
|
||||
it('filters single npm warn cleanup line', function () {
|
||||
const filter = createStderrFilter();
|
||||
filter.processChunk('npm warn cleanup Failed to remove some directories\n');
|
||||
filter.flush();
|
||||
|
||||
assert.deepStrictEqual(filter.getOutput(), []);
|
||||
});
|
||||
|
||||
it('filters multiple npm warn cleanup lines', function () {
|
||||
const filter = createStderrFilter();
|
||||
filter.processChunk(
|
||||
"npm warn cleanup 'C:\\\\Users\\\\...\\\\node_modules\\\\@kaitranntt\\\\.ccs-VzVYv4mp',\n" +
|
||||
"npm warn cleanup [Error: EPERM: operation not permitted, unlink '...\\\\bcrypt.node']\n" +
|
||||
'npm warn cleanup errno: -4048,\n' +
|
||||
"npm warn cleanup code: 'EPERM',\n" +
|
||||
"npm warn cleanup syscall: 'unlink',\n"
|
||||
);
|
||||
filter.flush();
|
||||
|
||||
assert.deepStrictEqual(filter.getOutput(), []);
|
||||
});
|
||||
|
||||
it('preserves non-cleanup npm warnings', function () {
|
||||
const filter = createStderrFilter();
|
||||
filter.processChunk('npm warn deprecated lodash@1.0.0: Use lodash@4.x instead\n');
|
||||
filter.flush();
|
||||
|
||||
assert.deepStrictEqual(filter.getOutput(), ['npm warn deprecated lodash@1.0.0: Use lodash@4.x instead']);
|
||||
});
|
||||
|
||||
it('preserves npm errors', function () {
|
||||
const filter = createStderrFilter();
|
||||
filter.processChunk('npm ERR! code ENOTFOUND\n');
|
||||
filter.processChunk('npm ERR! network request failed\n');
|
||||
filter.flush();
|
||||
|
||||
assert.deepStrictEqual(filter.getOutput(), ['npm ERR! code ENOTFOUND', 'npm ERR! network request failed']);
|
||||
});
|
||||
|
||||
it('filters cleanup but preserves other warnings in mixed output', function () {
|
||||
const filter = createStderrFilter();
|
||||
filter.processChunk(
|
||||
'npm warn deprecated chalk@4.0.0: Deprecated\n' +
|
||||
'npm warn cleanup Failed to remove...\n' +
|
||||
'npm warn peer react@17: Use react@18\n' +
|
||||
'npm warn cleanup errno: -4048,\n'
|
||||
);
|
||||
filter.flush();
|
||||
|
||||
assert.deepStrictEqual(filter.getOutput(), [
|
||||
'npm warn deprecated chalk@4.0.0: Deprecated',
|
||||
'npm warn peer react@17: Use react@18',
|
||||
]);
|
||||
});
|
||||
|
||||
it('is case-insensitive for npm warn cleanup', function () {
|
||||
const filter = createStderrFilter();
|
||||
filter.processChunk('NPM WARN CLEANUP something\n');
|
||||
filter.processChunk('Npm Warn Cleanup another\n');
|
||||
filter.flush();
|
||||
|
||||
assert.deepStrictEqual(filter.getOutput(), []);
|
||||
});
|
||||
});
|
||||
|
||||
describe('chunk boundary handling', function () {
|
||||
it('handles warning split across chunks', function () {
|
||||
const filter = createStderrFilter();
|
||||
// Simulate "npm warn cleanup" split across two chunks
|
||||
filter.processChunk('npm war');
|
||||
filter.processChunk('n cleanup Failed to remove\n');
|
||||
filter.flush();
|
||||
|
||||
// Should still filter - line-buffering catches this
|
||||
assert.deepStrictEqual(filter.getOutput(), []);
|
||||
});
|
||||
|
||||
it('handles multiple lines split across chunks', function () {
|
||||
const filter = createStderrFilter();
|
||||
filter.processChunk('npm warn deprecated lodash\n');
|
||||
filter.processChunk('npm warn cleanup ');
|
||||
filter.processChunk('EPERM error\n');
|
||||
filter.processChunk('npm ERR! fatal\n');
|
||||
filter.flush();
|
||||
|
||||
assert.deepStrictEqual(filter.getOutput(), ['npm warn deprecated lodash', 'npm ERR! fatal']);
|
||||
});
|
||||
|
||||
it('buffers incomplete line until newline', function () {
|
||||
const filter = createStderrFilter();
|
||||
filter.processChunk('incomplete line without newline');
|
||||
|
||||
// Nothing output yet - still buffered
|
||||
assert.deepStrictEqual(filter.getOutput(), []);
|
||||
assert.strictEqual(filter.getBuffer(), 'incomplete line without newline');
|
||||
|
||||
// Complete the line
|
||||
filter.processChunk(' - now complete\n');
|
||||
assert.deepStrictEqual(filter.getOutput(), ['incomplete line without newline - now complete']);
|
||||
});
|
||||
|
||||
it('flushes remaining buffer on close', function () {
|
||||
const filter = createStderrFilter();
|
||||
filter.processChunk('final line no newline');
|
||||
filter.flush();
|
||||
|
||||
assert.deepStrictEqual(filter.getOutput(), ['final line no newline']);
|
||||
});
|
||||
|
||||
it('does not flush cleanup warning on close', function () {
|
||||
const filter = createStderrFilter();
|
||||
filter.processChunk('npm warn cleanup no newline');
|
||||
filter.flush();
|
||||
|
||||
assert.deepStrictEqual(filter.getOutput(), []);
|
||||
});
|
||||
});
|
||||
|
||||
describe('edge cases', function () {
|
||||
it('handles empty input', function () {
|
||||
const filter = createStderrFilter();
|
||||
filter.processChunk('');
|
||||
filter.flush();
|
||||
|
||||
assert.deepStrictEqual(filter.getOutput(), []);
|
||||
});
|
||||
|
||||
it('handles only newlines', function () {
|
||||
const filter = createStderrFilter();
|
||||
filter.processChunk('\n\n\n');
|
||||
filter.flush();
|
||||
|
||||
assert.deepStrictEqual(filter.getOutput(), ['', '', '']);
|
||||
});
|
||||
|
||||
it('handles Windows CRLF line endings', function () {
|
||||
const filter = createStderrFilter();
|
||||
filter.processChunk('npm warn cleanup EPERM\r\nnpm ERR! error\r\n');
|
||||
filter.flush();
|
||||
|
||||
// Note: \r remains but line is still filtered by pattern
|
||||
// The ERR line has \r at end but that's okay
|
||||
const output = filter.getOutput();
|
||||
assert.strictEqual(output.length, 1);
|
||||
assert.ok(output[0].includes('npm ERR! error'));
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user