diff --git a/CHANGELOG.md b/CHANGELOG.md index 3465953..a869df1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/README.md b/README.md index 819ecb8..c15b07e 100644 --- a/README.md +++ b/README.md @@ -82,7 +82,7 @@ npx parche-cli feedback . --pack --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-.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 diff --git a/bin/parche.js b/bin/parche.js index 36e2870..0be144a 100755 --- a/bin/parche.js +++ b/bin/parche.js @@ -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}`); diff --git a/package-lock.json b/package-lock.json index b7cef53..26d3cdf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "parche-cli", - "version": "0.8.5", + "version": "0.8.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "parche-cli", - "version": "0.8.5", + "version": "0.8.6", "license": "MIT", "dependencies": { "@babel/parser": "^7.28.0", diff --git a/package.json b/package.json index 50fa7e2..7817405 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/paths.js b/src/paths.js index 2b5c840..559b09b 100644 --- a/src/paths.js +++ b/src/paths.js @@ -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 diff --git a/src/report.js b/src/report.js index be70025..4d203f5 100644 --- a/src/report.js +++ b/src/report.js @@ -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'), diff --git a/test/arca-condicion-sospechosa.test.js b/test/arca-condicion-sospechosa.test.js index a194f35..e2615ff 100644 --- a/test/arca-condicion-sospechosa.test.js +++ b/test/arca-condicion-sospechosa.test.js @@ -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`/); diff --git a/test/arca-soap-ruido.test.js b/test/arca-soap-ruido.test.js index d2a2cb2..f175690 100644 --- a/test/arca-soap-ruido.test.js +++ b/test/arca-soap-ruido.test.js @@ -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/); }); @@ -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(); }); diff --git a/test/feedback-061.test.js b/test/feedback-061.test.js index b9ba7da..3a16173 100644 --- a/test/feedback-061.test.js +++ b/test/feedback-061.test.js @@ -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/); diff --git a/test/feedback-traccion.test.js b/test/feedback-traccion.test.js index cd7f176..5e97f5e 100644 --- a/test/feedback-traccion.test.js +++ b/test/feedback-traccion.test.js @@ -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); diff --git a/test/scripts-de-prueba.test.js b/test/scripts-de-prueba.test.js new file mode 100644 index 0000000..2cced1e --- /dev/null +++ b/test/scripts-de-prueba.test.js @@ -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 }); +});