Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@

Los cambios de cada versión de `parche-cli` y de la Action `Pepo08/parche@v0`. En npm están la 0.1.1, la 0.5.1, de la 0.6.0 a la 0.7.0, la 0.7.2, la 0.7.3, de la 0.7.5 a la 0.8.0 y de la 0.8.5 en adelante. La 0.1.0 y de la 0.2.0 a la 0.5.0 no se publicaron en npm; lo que traían está incluido en la 0.5.1. La 0.7.1 y la 0.7.4 tampoco: salieron junto con la 0.7.2 y la 0.7.5. De la 0.8.1 a la 0.8.4, tampoco: salieron juntas con la 0.8.5. La fecha de cada versión es la del cambio; la de publicación figura en npm y en los [Releases](https://github.com/Pepo08/parche/releases).

## 0.8.6 — 2026-11-07

Un script para probar a mano ya no genera un PR: cuenta como un test.

### Corregido
- **`pr` ya no abre un PR que solo toca un script de prueba.** Un archivo como `probar-arca.js`, `prueba.php`, `test-afip.js` o `ejemplo-factura.js` se usa para probar a mano y no es el código que corre. Un PR que cambiaba solo eso se leía como ruido aunque el cuerpo dijera que el servicio ya mandaba el campo: un mantenedor lo cerró así ("El sistema ya informa CondicionIVAReceptorId según el receptor"). Ahora estos archivos se tratan como los tests: el hallazgo sigue en el scan, y si no hay otro cambio, el texto queda como issue en `.parche/`. Se reconocen por el nombre: "probar", "probando", "prueba(s)", "testear" y "ejemplo(s)", solos o con algo después de `-`, `_` o `.`, y "test-", "demo-", "scratch-" y "playground-". `test_data.js`, `testimonios.js`, `contest.js` y `demo.js` siguen siendo código.

## 0.8.5 — 2026-11-04

`fix` y `pr` frenan con un flag que no conocen, antes de tocar nada. En npm, esta versión trae también todo lo de la 0.8.1 a la 0.8.4.
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ npx parche-cli feedback . --pack <id> --outcome merged|edited|rejected
`parche pr` abre el PR por la API de GitHub si hay `GITHUB_TOKEN` y un remote `origin` en github.com; si no (o con `--dry-run`), deja la rama, el commit y el cuerpo del PR en `.parche/pr-<pack>.md`. Si hay ítems para revisar, el PR se abre como borrador.

- **Desde un fork:** `parche pr . --push-remote fork` pushea la rama al remote `fork` (tu fork, con tus credenciales de git) y abre el PR contra `origin`.
- **Nunca deja el repo sucio:** si la verificación falla, descarta lo que escribió y borra su rama. Si no hay cambios de código, o solo cambiarían tests o mocks, no abre PR: deja el texto como issue en `.parche/`.
- **Nunca deja el repo sucio:** si la verificación falla, descarta lo que escribió y borra su rama. Si no hay cambios de código, o solo cambiarían tests, mocks o scripts para probar a mano (como `probar-arca.js`), no abre PR: deja el texto como issue en `.parche/`.
- **`.parche/` no va en git:** es lo que Parche escribe en cada corrida (el reporte, el inventario, el registro y los cuerpos de los PRs), y lo agrega a `.git/info/exclude`. Si ya lo commiteaste (por ejemplo, copiando una carpeta escaneada a un repo nuevo), Parche funciona igual: no lo cuenta como un cambio sin commitear, nunca lo incluye en sus PRs y te avisa. Para dejar de versionarlo: `git rm -r --cached .parche` y sumá `.parche/` al `.gitignore`.

### Qué cambia en las versiones nuevas de tus SDKs
Expand Down
5 changes: 3 additions & 2 deletions bin/parche.js
Original file line number Diff line number Diff line change
Expand Up @@ -299,13 +299,14 @@ async function main() {
git.discardChanges(root);
throw new Error(`La verificación falló para ${pack.id}; no se abre el PR y el repo queda como estaba.\n${JSON.stringify(verification, null, 2)}`);
}
// Si solo cambiarían tests o mocks no hay PR: el texto queda como issue, sin "Modifica 1 archivo" (frente B, punto 20).
// Si solo cambiarían tests, mocks o scripts de prueba no hay PR: el texto queda como issue, sin "Modifica 1 archivo"
// (frente B, puntos 20 y 34).
const reportOnly = !changedFiles.length || changedFiles.every(isTestFile);
// La Action avisa si usa el GITHUB_TOKEN por defecto (PARCHE_DEFAULT_TOKEN): los workflows del PR esperan aprobación.
const ciApproval = process.env.GITHUB_ACTIONS === 'true' && process.env.PARCHE_DEFAULT_TOKEN === 'true';
fs.writeFileSync(bodyPath, prBody({ pack, findings: result.findings, changed: reportOnly ? [] : result.changed, verification: reportOnly ? null : verification, ciApproval, context: result.context }));
if (reportOnly) {
const why = changedFiles.length ? 'solo cambiaría tests o mocks: no se abre un PR' : 'sin cambios de código';
const why = changedFiles.length ? 'solo cambiaría tests, mocks o scripts de prueba: no se abre un PR' : 'sin cambios de código';
git.discardChanges(root);
console.log(` ${summary(result.findings)}`);
console.log(` ${why[0].toUpperCase()}${why.slice(1)}; queda como issue o reporte: ${bodyPath}`);
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "parche-cli",
"version": "0.8.5",
"version": "0.8.6",
"description": "Dependabot para APIs: detecta cambios de cualquier API de terceros en tu código (ARCA, Mercado Pago, OpenAI, Anthropic) y abre el PR con el arreglo.",
"keywords": [
"api",
Expand Down
18 changes: 15 additions & 3 deletions src/paths.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,27 @@ export const SKIP_DIRS = new Set(['node_modules', 'vendor', '.git', 'dist', 'bui
// Rutas siempre con "/": van al reporte, al cuerpo del PR y al inventario.
export const toPosix = (p) => p.split(path.sep).join('/');

// Tests, mocks, fixtures y ejemplos: un PR que solo toca eso no le sirve al mantenedor, y la detección
// por texto en esos archivos es ruido (frente B, 16/10). También en los lenguajes de solo detección
// Scripts sueltos para probar a mano (probar-arca.js, prueba.php, test-afip.js, ejemplo-factura.js): no son el
// código que corre. Un PR que solo toca eso se lee como ruido aunque el cuerpo lo diga (Elitali1/proyectoPaginaWeb,
// PR #16, cerrado por el mantenedor el 5/11: "El sistema ya informa CondicionIVAReceptorId según el receptor").
// Va por el nombre del archivo, sin la extensión: "probar", "probando", "prueba(s)", "testear" y "ejemplo(s)", solos o
// seguidos de - _ o .; "test", "demo", "scratch" y "playground", solo con guion. Así "test_data.js", "testimonios.js",
// "contest.js" y "demo.js" siguen siendo código.
const PROBE_STEM = /^(probar|probando|prueba|pruebas|testear|ejemplo|ejemplos)([-_.].*)?$|^(test|demo|scratch|playground)-.+$/i;
export function isProbeScript(file) {
return PROBE_STEM.test(file.split('/').pop().replace(/\.[^.]+$/, ''));
}

// Tests, mocks, fixtures, ejemplos y scripts de prueba: un PR que solo toca eso no le sirve al mantenedor, y la
// detección por texto en esos archivos es ruido (frente B, 16/10). También en los lenguajes de solo detección
// (x_test.go, test_x.py, XTests.cs, proyecto.Tests/) y los helpers de tests (x.test.helpers.ts, frente B, punto 24).
export function isTestFile(file) {
return /(^|\/)(__tests__|__mocks__|tests?|specs?|mocks?|fixtures?|examples?)\//i.test(file)
|| /(^|\/)[^/]*\.tests?\//i.test(file)
|| /\.(test|spec|mock)(\.[\w-]+)*\.[cm]?[jt]sx?$/i.test(file)
|| /Tests?\.(php|cs|java|vb)$/.test(file)
|| /_test\.(go|py)$/.test(file) || /(^|\/)test_[^/]*\.py$/.test(file);
|| /_test\.(go|py)$/.test(file) || /(^|\/)test_[^/]*\.py$/.test(file)
|| isProbeScript(file);
}

// Copias de respaldo: no es el código que corre. Un PR sobre BAK/ o sobre "factura copy.php" no le sirve al
Expand Down
4 changes: 2 additions & 2 deletions src/report.js
Original file line number Diff line number Diff line change
Expand Up @@ -140,13 +140,13 @@ export function prBody({ pack, findings, changed, verification, ciApproval = fal
const applied = fs.filter((f) => f.status === 'auto' || f.status === 'needs-review');
out.push(changed.length
? `Modifica ${changed.length} archivo(s): ${changed.map((x) => `\`${x.file}\``).join(', ')}.`
: !fs.length ? 'Solo aparece en tests o mocks: no hay nada para cambiar en el código que corre.'
: !fs.length ? 'Solo aparece en tests, mocks o scripts de prueba: no hay nada para cambiar en el código que corre.'
: fs.every((f) => f.status === 'detect-only') ? 'Parche no encontró comprobantes sin el campo. Esto es lo que conviene revisar.'
: 'No hay un arreglo automático seguro: esto es lo que hay que resolver a mano.', '');
const note = changed.length ? alreadyUsesNote(pack, context, changed) : null;
if (note) out.push(note, '');
for (const f of applied) out.push(`- ${LABEL[f.status]} \`${f.file}:${f.line}\` — ${f.message}`);
if (inTests.length) out.push(`- En tests o mocks (Parche no los toca): ${inTests.map((f) => `\`${f.file}:${f.line}\``).join(', ')}.`);
if (inTests.length) out.push(`- En tests, mocks o scripts de prueba (Parche no los toca): ${inTests.map((f) => `\`${f.file}:${f.line}\``).join(', ')}.`);
// Lo que falta: los hallazgos sin arreglo y, de cada arreglo parcial, la parte que no cubre.
const pending = [
...fs.filter((f) => f.status === 'needs-input' || f.status === 'blocked'),
Expand Down
5 changes: 3 additions & 2 deletions test/arca-condicion-sospechosa.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -406,12 +406,13 @@ test('sin nada para arreglar no se abre un PR: queda como issue, y la Action lo
});

test('al lado de un arreglo: el PR lo cuenta aparte, no pasa a borrador y no lo marca como cubierto', () => {
const { root, env, cleanup } = gitRepo({ 'src/s.js': SERVICE, 'probar.js': SCRIPT });
// El archivo sin el campo es código (src/caja.js): un script de prueba no abre PR (punto 34).
const { root, env, cleanup } = gitRepo({ 'src/s.js': SERVICE, 'src/caja.js': SCRIPT });
const summaryFile = path.join(root, '..', `${path.basename(root)}.md`);
const out = execFileSync('node', [BIN, 'pr', root, '--dry-run', '--pack', pack.id], { encoding: 'utf8', env: { ...env, GITHUB_ACTIONS: 'true', GITHUB_STEP_SUMMARY: summaryFile } });
assert.doesNotMatch(out, /iría como borrador/);
const body = fs.readFileSync(path.join(root, '.parche', `pr-${pack.id}.md`), 'utf8');
assert.match(body, /Modifica 1 archivo\(s\): `probar\.js`/);
assert.match(body, /Modifica 1 archivo\(s\): `src\/caja\.js`/);
assert.match(body, /El repo ya usa `CondicionIVAReceptorId` en `src\/s\.js`/);
assert.match(body, /### Condición frente al IVA para revisar/);
assert.match(body, /🔎 detectado `src\/s\.js:3`/);
Expand Down
10 changes: 6 additions & 4 deletions test/arca-soap-ruido.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -135,13 +135,13 @@ test('punto 27: un issue no lista como automático lo que caería en un test', (
const body = prBody({ pack, findings: [NEEDS, AUTO_TEST], changed: [], verification: null });
assert.match(body, /No hay un arreglo automático seguro/);
assert.doesNotMatch(body, /✅ automático/, 'ni en la lista ni en el resumen del pie');
assert.match(body, /En tests o mocks \(Parche no los toca\): `src\/lib\/mock-client\.test\.ts:60`\./);
assert.match(body, /En tests, mocks o scripts de prueba \(Parche no los toca\): `src\/lib\/mock-client\.test\.ts:60`\./);
assert.match(body, /`src\/factura\.js:8`/);
});

test('punto 27: si solo aparece en un test, el issue lo dice y no pide nada', () => {
const body = prBody({ pack, findings: [AUTO_TEST], changed: [], verification: null });
assert.match(body, /Solo aparece en tests o mocks/);
assert.match(body, /Solo aparece en tests, mocks o scripts de prueba/);
assert.doesNotMatch(body, /No hay un arreglo automático seguro/);
assert.doesNotMatch(body, /✅ automático/);
});
Expand Down Expand Up @@ -180,14 +180,16 @@ test('punto 28: un PR en un repo que ya usa el campo dice dónde y qué cubre',
});

test('punto 28: de punta a punta, con `pr --dry-run`', () => {
// El archivo sin el campo es código (src/caja.js). Si fuera un script de prueba (probar.js), no habría PR: punto 34,
// en scripts-de-prueba.test.js.
const { root, env, cleanup } = gitRepo({
'src/services/arca.js': 'export const a = (x, n) => x.electronicBillingService.createVoucher({ DocTipo: 80, DocNro: n, CondicionIVAReceptorId: 1 });\n',
'probar.js': 'export const b = (x) => x.electronicBillingService.createVoucher({ CbteTipo: 6, DocTipo: 99, DocNro: 0 });\n',
'src/caja.js': 'export const b = (x) => x.electronicBillingService.createVoucher({ CbteTipo: 6, DocTipo: 99, DocNro: 0 });\n',
});
execFileSync('node', [BIN, 'pr', root, '--dry-run', '--pack', pack.id], { encoding: 'utf8', env });
const body = fs.readFileSync(path.join(root, '.parche', `pr-${pack.id}.md`), 'utf8');
assert.match(body, /El repo ya usa `CondicionIVAReceptorId` en `src\/services\/arca\.js`/);
assert.match(body, /Cubre lo que todavía no lo lleva: `probar\.js`/);
assert.match(body, /Cubre lo que todavía no lo lleva: `src\/caja\.js`/);
cleanup();
});

Expand Down
2 changes: 1 addition & 1 deletion test/feedback-061.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ test('punto 20: rutas largas en Windows, --author y el aviso de email público',
test('punto 20 (pendiente de 0.5.0): si solo cambiarían tests, el texto queda como issue, sin "Modifica 1 archivo"', () => {
const { root, env, cleanup } = repo({ 'test/mock-client.test.ts': PARTIAL.replace('docTipo', '99').replace('(afip, 99)', '(afip, docTipo)') });
const out = execFileSync('node', [BIN, 'pr', root, '--pack', pack.id, '--dry-run'], { encoding: 'utf8', env });
assert.match(out, /Solo cambiaría tests o mocks/);
assert.match(out, /Solo cambiaría tests, mocks o scripts de prueba/);
const body = fs.readFileSync(path.join(root, '.parche', `pr-${pack.id}.md`), 'utf8');
assert.match(body, /### Qué encontró Parche/);
assert.doesNotMatch(body, /Modifica \d+ archivo|### Qué hace este PR|### Verificación/);
Expand Down
2 changes: 1 addition & 1 deletion test/feedback-traccion.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ test('punto 3: si la verificación falla, el repo queda limpio y sin la rama de
test('punto 11: si solo cambiarían tests o mocks, no se abre PR ni queda rama', () => {
const { root, g } = repo({ 'test/mock-afip.test.js': VOUCHER });
const out = execFileSync('node', [BIN, 'pr', root, '--pack', ARCA.id, '--dry-run'], { encoding: 'utf8' });
assert.match(out, /Solo cambiaría tests o mocks: no se abre un PR/);
assert.match(out, /Solo cambiaría tests, mocks o scripts de prueba: no se abre un PR/);
assert.equal(g('branch', '--list', 'parche/*').trim(), '');
assert.equal(g('status', '--porcelain').trim(), '');
for (const f of ['test/x.js', 'src/__mocks__/afip.js', 'a.spec.ts', 'lib/mock-client.test.ts', 'tests/FacturaTest.php', 'examples/demo.js']) assert.ok(isTestFile(f), f);
Expand Down
79 changes: 79 additions & 0 deletions test/scripts-de-prueba.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// Punto 34 de startup/traccion/feedback-producto.md (7/11/2026): el PR #16 de Elitali1/proyectoPaginaWeb solo tocaba
// probar-arca.js, un script para probar ARCA a mano, y el servicio ya mandaba el campo. El mantenedor lo cerró:
// "El sistema ya informa CondicionIVAReceptorId según el receptor". Un script de prueba cuenta como un test.
import { test } from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { execFileSync } from 'node:child_process';
import { loadPacks } from '../src/packs.js';
import { run } from '../src/engine.js';
import { isProbeScript, isTestFile } from '../src/paths.js';

const HERE = path.dirname(fileURLToPath(import.meta.url));
const BIN = path.join(HERE, '..', 'bin', 'parche.js');
const ARCA = loadPacks().find((p) => p.id === 'arca.wsfe.condicion-iva-receptor');

function repo(files) {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'parche-probe-'));
for (const [rel, text] of Object.entries(files)) {
fs.mkdirSync(path.dirname(path.join(root, rel)), { recursive: true });
fs.writeFileSync(path.join(root, rel), text);
}
const g = (...a) => execFileSync('git', a, { cwd: root, encoding: 'utf8' });
g('init', '-q', '-b', 'main');
g('-c', 'user.name=t', '-c', 'user.email=t@t', 'add', '-A');
g('-c', 'user.name=t', '-c', 'user.email=t@t', 'commit', '-q', '-m', 'init');
return { root, g };
}

// El servicio ya manda el campo; el script de prueba, no (como en Elitali1).
const SERVICE = 'export const emitir = (afip) => afip.ElectronicBilling.createVoucher({\n CbteTipo: 6,\n DocTipo: 99,\n DocNro: 0,\n CondicionIVAReceptorId: 5,\n});\n';
const PROBE = 'const probar = async (afip) => {\n const r = await afip.ElectronicBilling.createVoucher({\n CbteTipo: 6,\n DocTipo: 99,\n DocNro: 0,\n });\n console.log(r);\n};\nprobar(globalThis.afip);\n';

test('isProbeScript: los scripts sueltos para probar a mano, por el nombre', () => {
const probes = ['probar-arca.js', 'src/prueba.php', 'scripts/test-afip.js', 'ejemplo_factura.ts', 'pruebas.js', 'probando.mjs', 'Prueba-Factura.PHP', 'demo-arca.js', 'testear.js'];
for (const f of probes) {
assert.ok(isProbeScript(f), f);
assert.ok(isTestFile(f), f);
}
// Código que corre: no cuentan aunque el nombre se parezca, ni una carpeta con ese nombre.
const code = ['src/test_data.js', 'src/testimonios.js', 'contest.js', 'demo.js', 'src/probar/arca.js', 'src/pruebaDeVida.js', 'src/afip.service.ts', 'src/ejemplar.js', 'latest-arca.js'];
for (const f of code) {
assert.ok(!isProbeScript(f), f);
assert.ok(!isTestFile(f), f);
}
});

test('pr: si solo cambiaría un script de prueba, no abre un PR, no deja rama y el issue lo dice', () => {
const { root, g } = repo({ 'src/services/arca.service.js': SERVICE, 'probar-arca.js': PROBE });
const out = execFileSync('node', [BIN, 'pr', root, '--pack', ARCA.id, '--dry-run'], { encoding: 'utf8' });
assert.match(out, /Solo cambiaría tests, mocks o scripts de prueba: no se abre un PR/);
assert.equal(g('branch', '--list', 'parche/*').trim(), '', 'sin rama de Parche');
assert.equal(g('status', '--porcelain').trim(), '', 'sin archivos modificados');
// git puede pasarlo a CRLF al volver a main (core.autocrlf en Windows): se compara sin el fin de línea.
assert.equal(fs.readFileSync(path.join(root, 'probar-arca.js'), 'utf8').replace(/\r\n/g, '\n'), PROBE, 'el script queda como estaba');
const body = fs.readFileSync(path.join(root, '.parche', `pr-${ARCA.id}.md`), 'utf8');
assert.match(body, /Solo aparece en tests, mocks o scripts de prueba/);
assert.match(body, /En tests, mocks o scripts de prueba \(Parche no los toca\): `probar-arca\.js:2`/);
fs.rmSync(root, { recursive: true, force: true });
});

test('scan: el script de prueba sigue apareciendo, y el campo del servicio cuenta como "ya lo usa"', () => {
const { root } = repo({ 'src/services/arca.service.js': SERVICE, 'probar-arca.js': PROBE });
const result = run(root, { write: false, packIds: [ARCA.id] });
assert.ok(result.findings.some((f) => f.file === 'probar-arca.js'), 'el hallazgo del script no se pierde');
assert.deepEqual(result.context.fieldFiles, ['src/services/arca.service.js']);
fs.rmSync(root, { recursive: true, force: true });
});

test('pr: si el servicio también necesita el arreglo, el PR se abre y no toca el script de prueba', () => {
const service = SERVICE.replace(' CondicionIVAReceptorId: 5,\n', '');
const { root, g } = repo({ 'src/services/arca.service.js': service, 'probar-arca.js': PROBE });
const out = execFileSync('node', [BIN, 'pr', root, '--pack', ARCA.id, '--dry-run'], { encoding: 'utf8' });
assert.doesNotMatch(out, /no se abre un PR/);
assert.match(g('show', '--stat', '--format=', 'parche/' + ARCA.id), /src\/services\/arca\.service\.js/);
fs.rmSync(root, { recursive: true, force: true });
});
Loading