From 65365412db79e4dc57ac34ccc49c125bfdd6a4ec Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 1 Sep 2026 17:06:49 +0000 Subject: [PATCH 1/2] =?UTF-8?q?fix:=20EditEntryDialog=20para=20de=20ler=20?= =?UTF-8?q?err.message,=20mascarado=20em=20produ=C3=A7=C3=A3o?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit O único err.message que sobrava no repo (o resto foi convertido na #34) sempre exibe o parágrafo genérico do React em produção, porque erro que atravessa a fronteira de Server Action chega ao cliente reconstruído como Error genérico. A string pt-BR já existia no arquivo e nunca era exibida. Closes #122 --- __tests__/unit/no-err-message.test.ts | 48 ++++++++++++++++++++++++ components/devedores/EditEntryDialog.tsx | 3 +- 2 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 __tests__/unit/no-err-message.test.ts diff --git a/__tests__/unit/no-err-message.test.ts b/__tests__/unit/no-err-message.test.ts new file mode 100644 index 0000000..06c35bf --- /dev/null +++ b/__tests__/unit/no-err-message.test.ts @@ -0,0 +1,48 @@ +import { describe, it, expect } from 'vitest' +import { readFileSync, readdirSync, statSync } from 'node:fs' +import { join } from 'node:path' + +// Gate por string sobre o código-fonte (#122). `err.message`/`error.message` +// dentro de um `catch` de componente é sempre o parágrafo genérico do React +// em produção — erro que atravessa a fronteira de Server Action chega ao +// cliente como um `Error` reconstruído por `resolveErrorProd`, então +// `err instanceof Error` é sempre verdadeiro e `.message` nunca é a +// mensagem lançada pela action. Precedente: __tests__/unit/row-actions.test.ts +// (mesma técnica de readFileSync + asserção sobre o conteúdo do arquivo). + +const ROOT = process.cwd() +const SCAN_DIRS = ['app', 'components'] +const IGNORED_DIRS = new Set(['node_modules', '.next', '.git']) + +function collectTsxFiles(dir: string): string[] { + const entries = readdirSync(dir) + const files: string[] = [] + for (const entry of entries) { + if (IGNORED_DIRS.has(entry)) continue + const fullPath = join(dir, entry) + const stat = statSync(fullPath) + if (stat.isDirectory()) { + files.push(...collectTsxFiles(fullPath)) + } else if (entry.endsWith('.tsx')) { + files.push(fullPath) + } + } + return files +} + +// Âncora em `catch` para não pegar `errors.message`/`next.message` usados +// como chave de mapa de erros de formulário (ex: FeedbackDialog.tsx), que +// não têm relação com mensagem de exceção mascarada em produção. +const CATCH_ERR_MESSAGE = /catch[\s\S]{0,200}?\b(?:err|error|e)\s*\.\s*message\b/ + +describe('nenhum componente exibe err.message em catch (#122)', () => { + it('a mensagem de erro de Server Action é mascarada em produção — ler err.message nunca mostra a mensagem real', () => { + const files = [...SCAN_DIRS.flatMap((dir) => collectTsxFiles(join(ROOT, dir)))] + + const ofensores = files + .filter((file) => CATCH_ERR_MESSAGE.test(readFileSync(file, 'utf-8'))) + .map((file) => file.replace(ROOT + '/', '')) + + expect(ofensores).toEqual([]) + }) +}) diff --git a/components/devedores/EditEntryDialog.tsx b/components/devedores/EditEntryDialog.tsx index 0d247e0..48a8ddb 100644 --- a/components/devedores/EditEntryDialog.tsx +++ b/components/devedores/EditEntryDialog.tsx @@ -71,7 +71,8 @@ export function EditEntryDialog({ entry, open, onOpenChange }: Props) { toast.success('Lançamento atualizado.') onOpenChange(false) } catch (err) { - toast.error(err instanceof Error ? err.message : 'Erro ao editar lançamento.') + console.error('[EditEntryDialog] updateDebtEntry falhou', err) + toast.error('Erro ao editar lançamento.') } }) } From 005b6b50f6268dbaad21423f3cd1a6c6c9641a1b Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 1 Sep 2026 19:21:43 +0000 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20ancora=20gate=20de=20err.message=20n?= =?UTF-8?q?o=20sink=20toast.*,=20n=C3=A3o=20numa=20dist=C3=A2ncia=20de=20c?= =?UTF-8?q?atch?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A janela catch[\s\S]{0,200}? ficava cega assim que um console.error(...) — o próprio remédio que este arquivo institui — empurrava o toast.error(...) para fora dela. O falso-positivo que a janela existia para evitar (errors.message, next.message como chave de mapa de formulário) não ocorre de fato: \b antes de err/error/e não casa no meio de "errors"/"next". Ancorar no sink (toast.*) em vez de numa distância é mais fiel ao achado da #122: o defeito não é ler err.message, é exibi-lo ao usuário. Aponta review em https://github.com/Guiroos/mare/pull/137 Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_019Wgc6FNhxJPveiPvq6PjQd --- __tests__/unit/no-err-message.test.ts | 31 ++++++++++++++++----------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/__tests__/unit/no-err-message.test.ts b/__tests__/unit/no-err-message.test.ts index 06c35bf..2df82ef 100644 --- a/__tests__/unit/no-err-message.test.ts +++ b/__tests__/unit/no-err-message.test.ts @@ -3,12 +3,12 @@ import { readFileSync, readdirSync, statSync } from 'node:fs' import { join } from 'node:path' // Gate por string sobre o código-fonte (#122). `err.message`/`error.message` -// dentro de um `catch` de componente é sempre o parágrafo genérico do React -// em produção — erro que atravessa a fronteira de Server Action chega ao -// cliente como um `Error` reconstruído por `resolveErrorProd`, então -// `err instanceof Error` é sempre verdadeiro e `.message` nunca é a -// mensagem lançada pela action. Precedente: __tests__/unit/row-actions.test.ts -// (mesma técnica de readFileSync + asserção sobre o conteúdo do arquivo). +// exibido via `toast.*` é sempre o parágrafo genérico do React em produção — +// erro que atravessa a fronteira de Server Action chega ao cliente como um +// `Error` reconstruído por `resolveErrorProd`, então `err instanceof Error` é +// sempre verdadeiro e `.message` nunca é a mensagem lançada pela action. +// Precedente: __tests__/unit/row-actions.test.ts (mesma técnica de +// readFileSync + asserção sobre o conteúdo do arquivo). const ROOT = process.cwd() const SCAN_DIRS = ['app', 'components'] @@ -30,17 +30,22 @@ function collectTsxFiles(dir: string): string[] { return files } -// Âncora em `catch` para não pegar `errors.message`/`next.message` usados -// como chave de mapa de erros de formulário (ex: FeedbackDialog.tsx), que -// não têm relação com mensagem de exceção mascarada em produção. -const CATCH_ERR_MESSAGE = /catch[\s\S]{0,200}?\b(?:err|error|e)\s*\.\s*message\b/ +// Âncora no sink (`toast.*(...)`), não numa distância de `catch`: o defeito +// da #122 não é ler `err.message`, é exibi-lo ao usuário, e `toast.*` é o +// sink em todos os sites que a #34 enumerou. Uma janela de distância a partir +// de `catch` fica cega assim que um `console.error(...)` (o próprio remédio +// que este arquivo institui) empurra o `toast.error(...)` para fora dela — +// e o falso-positivo que a janela existia para evitar (`errors.message`, +// `next.message` como chave de mapa de erros de formulário) já não ocorre: +// `\b` antes de `err`/`error`/`e` não casa no meio de "errors"/"next". +const TOAST_ERR_MESSAGE = /toast\.\w+\([^)]*\b(?:err|error|e)\s*\.\s*message\b/ -describe('nenhum componente exibe err.message em catch (#122)', () => { +describe('nenhum componente exibe err.message em toast (#122)', () => { it('a mensagem de erro de Server Action é mascarada em produção — ler err.message nunca mostra a mensagem real', () => { - const files = [...SCAN_DIRS.flatMap((dir) => collectTsxFiles(join(ROOT, dir)))] + const files = SCAN_DIRS.flatMap((dir) => collectTsxFiles(join(ROOT, dir))) const ofensores = files - .filter((file) => CATCH_ERR_MESSAGE.test(readFileSync(file, 'utf-8'))) + .filter((file) => TOAST_ERR_MESSAGE.test(readFileSync(file, 'utf-8'))) .map((file) => file.replace(ROOT + '/', '')) expect(ofensores).toEqual([])