mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-01 04:09:17 +00:00
fix(process): слияние не обрывает дифф кандидата и называет причину сбоя запуска
Шаг слияния конвейера считает patch-id материала и кандидата через `git diff --full-index`, когда `dev` сдвинулся за время ревью. Дифф задачи, пересобирающей бандл, несёт три копии `houseplan-card.js` — у #594 это 7 103 616 байт. `sh()` не задавал `maxBuffer`, умолчание `spawnSync` — 1 МиБ, и процесс убивался по ENOBUFS на 1 027 868 байтах. Вторая половина дефекта прятала первую: `r.error` нигде не читался, а `status: r.status ?? 1` выдавало `null` за «git вернул 1». `must()` брал `r.stderr || r.stdout`, stderr был пуст — и в issue уезжало «Слияние не выполнено: diff: <огрызок диффа>». Огрызок выглядит осмысленным и уводит разбор в сторону. Путь исполняется только при сдвинувшемся `dev`, поэтому дефект дремал. Но конвейер сам пушит в `dev` документ ревью на каждом раунде, так что `devMoved` — норма, а не исключение. Что изменено: - `MAX_COMMAND_OUTPUT_BYTES` (256 МиБ) и явный `maxBuffer` в `sh()`; - `sh()` называет сбой запуска в `stderr` результата вместо молчания; - `sh()` экспортирован, у обоих пунктов есть исполнимый свидетель; - мутант `merge-candidate-truncates-the-candidate-diff` возвращает 1 МиБ. Issue: #596 User-Visible: no
This commit is contained in:
@@ -99,9 +99,25 @@ export function commentFor(action, ctx) {
|
||||
// ---------------------------------------------------------------------------
|
||||
// Исполнение: git + gh через `ops`, чтобы тест подменял их целиком.
|
||||
|
||||
const sh = (cmd, args, opts = {}) => {
|
||||
const r = spawnSync(cmd, args, { encoding: 'utf8', ...opts });
|
||||
return { status: r.status ?? 1, stdout: (r.stdout || '').trim(), stderr: (r.stderr || '').trim() };
|
||||
/**
|
||||
* #596: сколько вывода готов принять один вызов. Умолчание `spawnSync` — 1 МиБ,
|
||||
* а `git diff` кандидата несёт три копии бандла: у #594 это 7,1 МБ. Процесс
|
||||
* убивался по ENOBUFS, `status` становился `null`, и `status ?? 1` выдавало это
|
||||
* за «git вернул 1» — с УСЕЧЁННЫМ stdout в сообщении об ошибке. Разбор уходил
|
||||
* в сторону: огрызок диффа выглядит осмысленным.
|
||||
*/
|
||||
export const MAX_COMMAND_OUTPUT_BYTES = 256 * 1024 * 1024;
|
||||
|
||||
/**
|
||||
* Запуск с двумя гарантиями: вывод не обрезается молча, а сбой самого запуска
|
||||
* не выдаёт себя за ненулевой код возврата. `r.error` (ENOBUFS, ENOENT, таймаут)
|
||||
* уезжает в `stderr` результата, откуда его печатает `must()`.
|
||||
*/
|
||||
export const sh = (cmd, args, opts = {}) => {
|
||||
const r = spawnSync(cmd, args, { encoding: 'utf8', maxBuffer: MAX_COMMAND_OUTPUT_BYTES, ...opts });
|
||||
const failure = r.error ? `${cmd} не выполнился: ${r.error.code || r.error.message}` : '';
|
||||
const stderr = [failure, (r.stderr || '').trim()].filter(Boolean).join('\n');
|
||||
return { status: failure ? 1 : (r.status ?? 1), stdout: (r.stdout || '').trim(), stderr };
|
||||
};
|
||||
|
||||
export function realOps({
|
||||
|
||||
@@ -9937,6 +9937,18 @@ const MUTANT_DEFINITIONS = [
|
||||
replace: ' const mismatch = evidenceMismatch(proof.evidence, proof.evidence);',
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'merge-candidate-truncates-the-candidate-diff',
|
||||
guard: 'node --test --test-name-pattern="#596" test/merge-candidate.test.mjs',
|
||||
because: '#596: дифф кандидата с тремя копиями бандла — семь мегабайт. Без явного maxBuffer '
|
||||
+ 'spawnSync убивает git по ENOBUFS, patch-id считается по огрызку, и слияние падает '
|
||||
+ 'с усечённым диффом вместо причины',
|
||||
patches: [{
|
||||
file: 'scripts/merge-candidate.mjs',
|
||||
find: " const r = spawnSync(cmd, args, { encoding: 'utf8', maxBuffer: MAX_COMMAND_OUTPUT_BYTES, ...opts });",
|
||||
replace: " const r = spawnSync(cmd, args, { encoding: 'utf8', maxBuffer: 1024 * 1024, ...opts });",
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'declared-baseline-review-run-never-checked',
|
||||
guard: 'node --test --test-name-pattern="#595" test/release-gate.test.mjs',
|
||||
|
||||
@@ -5,7 +5,9 @@ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
|
||||
import { MAX_ATTEMPTS, commentFor, decideMerge, mergeCandidate, realOps } from '../scripts/merge-candidate.mjs';
|
||||
import {
|
||||
MAX_ATTEMPTS, MAX_COMMAND_OUTPUT_BYTES, commentFor, decideMerge, mergeCandidate, realOps, sh,
|
||||
} from '../scripts/merge-candidate.mjs';
|
||||
import { buildCiProof } from '../scripts/ci-proof.mjs';
|
||||
|
||||
const mergeProofContext = (row, sha, tree) => {
|
||||
@@ -387,3 +389,24 @@ test('#516 AC1: dev moved only by review documents and the branch carries its ow
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
// #596: шаг слияния считает patch-id через `git diff` кандидата, а тот несёт три
|
||||
// копии бандла — семь мегабайт у #594. С умолчанием spawnSync в 1 МиБ процесс
|
||||
// убивался по ENOBUFS, `status` приходил `null`, и вывод обрезался посередине.
|
||||
test('#596: вывод больше мегабайта доезжает целиком, а не обрывается по буферу', () => {
|
||||
const bytes = 3 * 1024 * 1024;
|
||||
const r = sh(process.execPath, ['-e', `process.stdout.write('x'.repeat(${bytes}))`]);
|
||||
assert.equal(r.status, 0, r.stderr);
|
||||
assert.equal(r.stdout.length, bytes);
|
||||
assert.ok(MAX_COMMAND_OUTPUT_BYTES > bytes, 'предел выбран с запасом над проверяемым объёмом');
|
||||
});
|
||||
|
||||
// #596: вторая половина дефекта — сбой ЗАПУСКА выдавался за ненулевой код
|
||||
// возврата, и в сообщение уезжал усечённый stdout. Огрызок диффа выглядит
|
||||
// осмысленным и уводит разбор в сторону; причина обязана быть названа.
|
||||
test('#596: сбой запуска называет причину, а не притворяется кодом возврата', () => {
|
||||
const r = sh('houseplan-no-such-command-596', []);
|
||||
assert.equal(r.status, 1);
|
||||
assert.match(r.stderr, /houseplan-no-such-command-596 не выполнился: ENOENT/);
|
||||
assert.equal(r.stdout, '');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user