fix: concluir recuperacao de exportacoes e consolidar candidata QA
tests / ci (push) Canceled after 0s

This commit is contained in:
2026-10-21 08:43:12 +02:00
parent c22003a3da
commit 13f68f1bd8
8 changed files with 639 additions and 110 deletions
@@ -158,5 +158,166 @@
4
],
"remaining_scope": "Read-only remote collection; native PDF; new cashflow visual continuation; evidence closure; excludes waiting and tax-engine integration",
"secrets_and_private_fixtures": "Not included in source manifest or Git"
"secrets_and_private_fixtures": "Not included in source manifest or Git",
"current_reconciliation_20261021": {
"items": {
"AUD-SIM-INT01": {
"status": "Concluído no recorte",
"remaining": "Nenhum delta de validação de data por demonstrar; prova anterior preservada."
},
"AUD-SIM-INT02": {
"status": "Parcial",
"remaining": "Leitura remota concluída: zero regras de todos os estados na homologação, tal como QA; seis referências jurídicas não são parâmetros executáveis. Falta cadastro fundamentado por componente/âmbito/período e ligação funcional ao motor fiscal. Fontes e análises existentes reutilizáveis; aceitação oficial da Tempo de08/10 preservada. Não activar taxas nem importar/publicar por esta reconciliação."
},
"AUD-SIM-INT03": {
"status": "Concluído no recorte",
"remaining": "A/B, logout/novo login, recuperação e recusa pela aplicação demonstrados anteriormente. A tesouraria tem prova API de nova sessão e prova visual de recuperação por recarga. Não atribuir ao navegador nova sessão dos valores de tesouraria, nem nova certificação RLS; limites expressos conservados."
},
"AUD-SIM-INT04": {
"status": "Concluído no recorte demonstrado",
"remaining": "Histórico24/12 visível, hashes preservados e versões temporais sintéticas do resolvedor conferidas. Execução fiscal integrada com parâmetros reais pertence ao saldo de02 e não foi demonstrada por estes testes."
},
"AUD-SIM-INT05": {
"status": "Concluído nos controlos delimitados",
"remaining": "Controlos independentes de caixa/plano/crédito, zero/desconhecido e moedas preservados. Não são validação de taxas legais nem certificação de toda a contabilidade."
},
"AUD-SIM-INT06": {
"status": "Concluído na apresentação e comparação deste relatório",
"remaining": "Rodapé do PDF nativo fechado em QA segundo provas apresentadas;10 páginas/valores/CSV não repetidos. Tempo declarou que não abriu imagem/PDF pelos caminhos locais: registar aceitação das provas, não revisão visual directa do utilizador. Outras provas de relatório conservadas; falhas interactivas de exportação/anexos mantêm saldo em07."
},
"AUD-SIM-INT07": {
"status": "Parcial",
"remaining": "Gravação503→nova tentativa→recuperação e exportações normais demonstradas. Faltam falhas dirigidas de geração Word/ZIP/PDF e retoma; teste interactivo de limite de anexo não ocorreu por bloqueio da ferramenta antes do envio. Nova leitura do código: três catch apresentam cause.message para Error, podendo mostrar texto técnico sem tradução. Sem nova falha de execução alegada."
},
"AUD-SIM-INT08": {
"status": "Parcial",
"remaining": "Inventário estático,404 real e traduções de validação preservados. Classificar/apresentar em português os erros dinâmicos das bibliotecas de exportação (cause.message, três consumidores) e conferir estados residuais/governação delimitados; não certificar todos os textos dinâmicos do catálogo."
}
},
"remaining_hours_delimited": [
1,
2
],
"integral_fiscal_implementation_estimated": false,
"user_direct_visual_review_of_local_files": false,
"native_footer_accepted_on_presented_evidence": true,
"remote_rule_count": 0,
"remote_legal_reference_count": 6,
"supersedes_prior_pending": [
"remote authentication and inventory",
"native print save and visual footer correction",
"cashflow screen/download values"
],
"tool_limits_retained": [
"interactive file chooser before attachment upload",
"native save performed by user"
],
"branch": "codex/simulator-integrated-review",
"main_merge": false,
"read_only_evidence_sha256": "bbb2d2f0b001f1f439ce2bf8e60a4233467463f5cfa430dd05b82f515f5d9a5f"
},
"candidate_20261021": {
"branch": "codex/simulator-integrated-review",
"base": "c22003a3",
"main_integration": false,
"active_changes": false,
"new_tests": 4,
"typescript": "passed",
"browser": {
"word_failure_retry": "passed, 2 attempts / 1 success",
"pdf_failure_retry": "passed, failed popup closed / one populated retry",
"zip_failure_retry": "passed: failed ZIP, edited title/unchecked section and one attachment retained; retry produced one valid archive",
"attachment_limit_ui": "passed: refused16MB, prior valid entry retained",
"attachment_valid_retry": "passed: duplicate selection leaves one entry and translated status"
},
"duplicate_prevention": "synchronous gate test passed; attachment name+SHA256 identity test passed; ZIP single original test passed",
"logs": "local diagnostic console; no backend telemetry added",
"accepted_evidence_reused": [
"native PDF 10-page footer QA acceptance",
"financial values and CSV",
"A/B and new session",
"historical snapshots and temporal resolver",
"read-only remote fiscal inventory"
],
"limitations": [
"Synthetic wrapper injects generator faults only and is excluded from deployment",
"Native print and values/CSV proofs reused, not repeated",
"Transient attachment read/concurrent-click proofs are automated; manual chooser path observed separately",
"Local console diagnostics are not persistent server audit",
"Fiscal integration remains incomplete; catalogue/governance language is not globally certified"
],
"sources": [
{
"path": "resources/js/components/project-dossier-export.tsx",
"sha256": "c0bbea29c8e307785cbe54014a85d6271c723a541bea9f2ad5196118b9570764"
},
{
"path": "resources/js/lib/dossier-export-state.ts",
"sha256": "ac40e30134ba56b67c6c6a3b044aaeb110750cac53e0934794c8a7d346d745fe"
},
{
"path": "resources/js/lib/project-dossier-document.ts",
"sha256": "3bec7565324587bdd42f65f5b3c723597ec554b1c80376bfb45d350516eae16f"
},
{
"path": "tests/frontend/dossier-export-recovery.test.mjs",
"sha256": "b4112d612ce204c41b7376e64a0c15e7b85a6ec19087b0ed5415c1114d99f2b2"
},
{
"path": "tests/frontend/project-dossier-report.test.mjs",
"sha256": "77e33425e33e7ca0290e60e0b09fab7bbe5c8bd1eef196dd0863452da8f06271"
}
],
"remaining_engineering_hours_delimited": [
0,
0
],
"review_pending": true,
"build_manifest_sha256": "e4846e02db6112bce9c1f5466038e0a25f381c17fdcd8fe54248a7068a0645dd",
"evidence": [
{
"path": "outputs\\simulator-continuation-20261020\\export-word-error-20261021.txt",
"sha256": "b1c984663ea27bbeb5bfe760ab181314142f32cc9f6b27fa11c2fb7d7f0456ca"
},
{
"path": "outputs\\simulator-continuation-20261020\\export-pdf-error-20261021.txt",
"sha256": "b46f597e185874875d823d2c90219b555ee1f2028b5d144e00754db73468ed41"
},
{
"path": "outputs\\simulator-continuation-20261020\\export-pdf-retry-20261021.txt",
"sha256": "091679de9a60e4f22e2d0cb2127b736addfc9e2752fb0bb2aec051ddcb51f3c8"
},
{
"path": "outputs\\simulator-continuation-20261020\\export-zip-error-20261021.txt",
"sha256": "eda788b6f69b7fbcbb13aadf44dd0d3fbe695a2f050445b6f6a6d6553e7dc63d"
},
{
"path": "outputs\\simulator-continuation-20261020\\export-zip-retry-20261021.txt",
"sha256": "ef8c3c2197cb77260c1f8a1f169f282f1a14376e2aa331d26e9b98aee45e92c3"
},
{
"path": "outputs\\simulator-continuation-20261020\\attachment-limit-preserved-20261021.txt",
"sha256": "2132c4ddfaf044e8349766322514e9c6ce5c1a0f06236bfb4323b8a010e73197"
},
{
"path": "outputs\\simulator-continuation-20261020\\attachment-duplicate-retry-20261021.txt",
"sha256": "3027ea5949168df879442bb04a481d3f8c5e4741e470d30875e1dc6c25892312"
},
{
"path": "outputs\\simulator-continuation-20261020\\export-zip-content-proof-20261021.json",
"sha256": "6ee6bece4d2110c996bad2d686b24970c9d199eff2940dfba34e285f005f0f7c"
}
],
"build_exit_code": 0,
"existing_items_current_scope": {
"AUD-SIM-INT01": "demonstrated within bounded scope; see existing manual for limits",
"AUD-SIM-INT02": "partial; fiscal implementation remains open",
"AUD-SIM-INT03": "demonstrated within bounded scope; see existing manual for limits",
"AUD-SIM-INT04": "demonstrated within bounded scope; see existing manual for limits",
"AUD-SIM-INT05": "demonstrated within bounded scope; see existing manual for limits",
"AUD-SIM-INT06": "demonstrated within bounded scope; see existing manual for limits",
"AUD-SIM-INT07": "demonstrated within bounded scope; see existing manual for limits",
"AUD-SIM-INT08": "demonstrated within bounded scope; see existing manual for limits"
}
}
}
@@ -1623,3 +1623,94 @@ Artefactos em `outputs/simulator-continuation-20261020/`: `interface-comparison.
Provas adicionais: teste HTTP com dois dossiers, logout lógico/nova sessão, recuperação exacta e recusa por outra conta passou (8 asserções), em transacção revertida no clone. Quatro controlos frontend dirigidos passaram, incluindo recuperação de mês vazio, valor inválido para correcção, relatório sem zero inventado e pressupostos completos do crédito. As 10 verificações do exportador afectado passaram; teste temporal sintético passou (7 asserções); erro HTTP em português passou (3). TypeScript/build e sintaxe PHP passaram. A conferência visual do novo componente de tesouraria com sessão real QA permanece por executar depois de fechar a impressão do Edge; não foi substituída por uma prova unitária.
**Saldo revisto após esse achado: 2–4 h de verificação delimitada**, já descontado o desenvolvimento concluído: leitura remota 0,5–1 h; PDF nativo 0,5–1 h; continuidade visual/saída da tesouraria recém-corrigida 0,5–1 h; consolidação das provas e limitações 0,5–1 h. Exclui espera por autenticação/ficheiro e a integração funcional futura das regras fiscais. Esta estimativa substitui 1–3 h acima por uma divergência concreta; não retoma as 6–10 h nem as 18–30 h originais.
### Retoma da conferência visual de tesouraria — 21/10/2026
[Retoma 21/10/2026] QA 18576, clone simulator_review_delta_20261020_test: sessão sintética renovada por login normal; gravação autenticada e recarga recuperaram saldo 123, receita 900, custos 600, calendário mensal 10/vazio/0, crédito IVA estimado 500. Caixa conhecida 3713 MZN; total indeterminado desde mês 2; crédito estimado excluído da caixa. Prova visual cashflow-recovered-20261021.png e snapshot .txt em outputs/simulator-continuation-20261020. Não certifica recuperação destes novos valores após outro login, CSV/DOCX ou PDF nativo. Ligação de controlo do navegador perdida durante espera de download; não atribuir falha à aplicação. Leitura remota aguarda identificação da ligação SSH; zero alterações no destino.
### Leitura fiscal remota e saída nativa — 21/10/2026
[Leitura SSH 21/10/2026 08:17 Maputo] Zero regras em simulation_tax_rule_versions na homologação, incluindo todos os estados por leitura administrativa sem ocultação RLS. Seis referências em investment_legal_sources; não são parâmetros executáveis. QA também tinha zero regras. BEGIN READ ONLY/SELECT/ROLLBACK; zero escritas. Prova homologation-fiscal-readonly.json. O git HEAD registado é do checkout consultado, não certifica a revisão do build servido. A ausência de cadastro não é ausência de fontes oficiais nem reabre a aceitação da Tempo de 08/10. Não implementa a ligação funcional ao motor fiscal.
[Exportação nativa 21/10/2026] Saída actual aberta pelo botão do portal: revisão 4, 12 meses, MZN, saldo inicial123/receita900/custos600, calendário10/vazio/0, caixa conhecida3713 e total indeterminado coincidem com o ecrã. Crédito estimado500 identificado como pressuposto, não somado à caixa. Prova portal-print-current-20261021.txt. Relatório de20/10/revisão2 preservado, sem o confundir com o actual. Downloads: sem novo CSV na pasta habitual ou nos temporários locais conferidos; isso não demonstra falha nem cancelamento. Histórico de downloads bloqueado pela política da ferramenta; caminho do ficheiro solicitado antes de repetir. PDF nativo aberto, mas paginação/legibilidade aguardam o ficheiro efectivamente guardado pelo Edge; conversão DOCX/PDF anterior não conta como essa prova. Perda da ligação e bloqueio de controlo durante impressão são limitações da ferramenta, sem falha do simulador demonstrada.
### Ficheiros de Transferências recuperados — 21/10/2026
[Fecho da verificação dos ficheiros 21/10/2026] Transferências Windows redireccionada para \\Mac\Home\Downloads; CSV concluído08:12:46, PDF nativo08:17:19. Sem repetir downloads. Os12 registos mensais do CSV coincidem exactamente com o snapshot do ecrã após correspondência de colunas; PDF revisão4 conserva valores mensais, MZN,12meses,123/900/600, crédito estimado500 não caixa e caixa conhecida3713. Os10 PNG do PDF nativo foram inspeccionados: defeito confirmado na linha repetida de rodapé, sobreposta ao corpo perto do topo de páginas, incluindo2,6e10. Conteúdo/valores conferidos; paginação/legibilidade não aceites até correcção e nova prova nativa. Não é conversão DOCX/PDF. Perda de ligação continua limitação da ferramenta e não causou falha do download; sobreposição é defeito distinto demonstrado no PDF. Provas: download-native-inspection-20261021.json, portal-cashflow-20261021.csv, portal-native-20261021.pdf e native-pdf-render/. A indicação anterior de ficheiros pendentes fica superada por esta prova. Sem alteração funcional nem escrita remota.
### Correcção do rodapé do PDF nativo em QA — 21/10/2026
[Correcção do rodapé nativo QA 21/10/2026] Origem: pedido expresso da Tempo nesta conversa; solução de implementação, sem regra organizacional nova. Causa em createProjectDossierPrintHtml (project-dossier-document.ts): footer position:fixed/bottom:-14mm sobrepunha a área fragmentada de impressão. Substituído na impressão por caixas @page @bottom-left/@bottom-right, dentro da margem inferior reservada20mm, com identificação/revisão e número de página/total. Rodapé normal de ecrã conservado; sem diminuir letras do corpo (10,5pt), tabelas, cabeçalhos ou origens (9pt), nem retirar conteúdo. Mesmo dossier B/revisão4 regenerado pelo botão do portal QA18576 e guardado no Edge, não por conversão DOCX. 10→10 páginas A4; comparação automática do corpo por página, descontando apenas hora de emissão/rodapé/contador, coincide integralmente. Inspecção visual dos10 PNG: sem sobreposição, cabeçalhos de tabela repetidos, quebras/linhas preservadas, última página íntegra e numeração1/10→10/10. Rodapés medidos inteiramente na margem inferior. Ecrã QA conferido após build; estilos screen e geradores DOCX/ZIP inalterados por comparação de fontes. 10 testes do exportador, TypeScript e build passaram; escape da revisão na nova string CSS verificado. Reutilizadas provas de valores e CSV aceites; não repetido download/aritmética. Provas: native-footer-fix-20261021.json; portal-native-fixed-20261021.pdf; native-footer-before-after-20261021.jpg; native-pdf-fixed-render/; screen-after-print-fix-20261021.png. Fechado exclusivamente o defeito de rodapé neste relatório QA; limitações e restantes critérios de AUD-SIM-INT06 mantêm âmbito próprio. Relato anterior de sobreposição fica superado neste delta pela comparação antes/depois. Branch codex/simulator-integrated-review separada de main, sem integração, escrita remota ou disponibilização activa; revisão prevista mantém-se pendente.
### Saldo consolidado dos oito itens — 21/10/2026
Reconciliação a pedido expresso da Tempo. O fecho do rodapé limita-se à apresentação deste relatório e baseia-se nas provas apresentadas; o utilizador declarou não ter conferido a imagem nem o PDF pelos caminhos locais. Não é fecho da revisão integral do simulador. Reutilizadas as provas aceites, sem repetir10 páginas, valores,CSV ou consulta SSH.
| Item existente | Estado no recorte | Saldo / limite |
|---|---|---|
| AUD-SIM-INT01 | Concluído no recorte | Nenhum delta de validação de data por demonstrar; prova anterior preservada. |
| AUD-SIM-INT02 | Parcial | Leitura remota concluída: zero regras de todos os estados na homologação, tal como QA; seis referências jurídicas não são parâmetros executáveis. Falta cadastro fundamentado por componente/âmbito/período e ligação funcional ao motor fiscal. Fontes e análises existentes reutilizáveis; aceitação oficial da Tempo de08/10 preservada. Não activar taxas nem importar/publicar por esta reconciliação. |
| AUD-SIM-INT03 | Concluído no recorte | A/B, logout/novo login, recuperação e recusa pela aplicação demonstrados anteriormente. A tesouraria tem prova API de nova sessão e prova visual de recuperação por recarga. Não atribuir ao navegador nova sessão dos valores de tesouraria, nem nova certificação RLS; limites expressos conservados. |
| AUD-SIM-INT04 | Concluído no recorte demonstrado | Histórico24/12 visível, hashes preservados e versões temporais sintéticas do resolvedor conferidas. Execução fiscal integrada com parâmetros reais pertence ao saldo de02 e não foi demonstrada por estes testes. |
| AUD-SIM-INT05 | Concluído nos controlos delimitados | Controlos independentes de caixa/plano/crédito, zero/desconhecido e moedas preservados. Não são validação de taxas legais nem certificação de toda a contabilidade. |
| AUD-SIM-INT06 | Concluído na apresentação e comparação deste relatório | Rodapé do PDF nativo fechado em QA segundo provas apresentadas;10 páginas/valores/CSV não repetidos. Tempo declarou que não abriu imagem/PDF pelos caminhos locais: registar aceitação das provas, não revisão visual directa do utilizador. Outras provas de relatório conservadas; falhas interactivas de exportação/anexos mantêm saldo em07. |
| AUD-SIM-INT07 | Parcial | Gravação503→nova tentativa→recuperação e exportações normais demonstradas. Faltam falhas dirigidas de geração Word/ZIP/PDF e retoma; teste interactivo de limite de anexo não ocorreu por bloqueio da ferramenta antes do envio. Nova leitura do código: três catch apresentam cause.message para Error, podendo mostrar texto técnico sem tradução. Sem nova falha de execução alegada. |
| AUD-SIM-INT08 | Parcial | Inventário estático,404 real e traduções de validação preservados. Classificar/apresentar em português os erros dinâmicos das bibliotecas de exportação (cause.message, três consumidores) e conferir estados residuais/governação delimitados; não certificar todos os textos dinâmicos do catálogo. |
**Estimativa restante deste recorte:1–2h de engenharia**, excluindo espera por interacção necessária da ferramenta: 07/08 agrupados0,75–1,5h para tratamento e conferência dirigida de erros dinâmicos/retoma/anexos; 0,25–0,5h para reconciliação final das provas. Não somar o mesmo erro duas vezes por aparecer em07e08. Substitui o saldo anterior2–4h porque leitura remota, apresentação nativa e comparação da tesouraria já foram executadas. Não inclui cadastrar/aplicar regras fiscais reais, integrar fórmulas no motor ou certificar todo o catálogo. O esforço para02 integral permanece por dimensionar por componente, âmbito, período e correspondência documental; não há estimativa de conclusão integral obtida apenas deste1–2h.
Próxima execução delimitada: corrigir a apresentação de excepções brutas nos três exportadores, conferir falha/retoma e os limites interactivos que faltam, preservando os relatórios/CSV já aceites. Branch codex/simulator-integrated-review mantida separada de main até à revisão consolidada. Nenhuma nova concessão, regra fiscal, migration ou escrita remota.
### Candidata consolidada da revisão do simulador — 21/10/2026
Origem: pedido expresso da Tempo para fechar apenas falhas dirigidas de exportação/anexos, apresentação das mensagens e consolidação. As 1–2 h anteriores respeitavam a esse recorte, não à implementação fiscal integral. A branch `codex/simulator-integrated-review` permanece separada de `main`; não integrar antes da revisão consolidada porque a cadeia existente disponibiliza automaticamente a revisão integrada.
**Separação funcional preservada.** Os cálculos de planeamento usam valores declarados pelo utilizador, com origem e pressupostos identificados. O relatório não os transforma em resultados fiscais. Falta de imposto/parâmetro continua indeterminada, não zero; crédito/pedido/reconhecimento de IVA não se torna dinheiro recebido. A correcção de falhas de exportação e do rodapé não encerra a componente fiscal.
**Implementação deste delta.** O exportador apresenta mensagens em português, identifica a operação e a referência de diagnóstico e mantém as selecções para nova tentativa pelo mesmo botão. A excepção original é registada na consola técnica local com operação/referência; não há novo envio ao servidor, telemetria nem registo de conteúdo do dossier/anexos. A consola é um diagnóstico local, não uma auditoria persistente no servidor. Uma guarda síncrona recusa outra execução enquanto a primeira está activa, mesmo antes da actualização do estado React; após erro a guarda é libertada. Preparação dos anexos é atómica: falhas/limites não substituem a lista existente nem as legendas. A selecção repetida do mesmo nome com os mesmos bytes mantém uma entrada; bytes diferentes com o mesmo nome e nomes diferentes continuam distintos. Esta identidade é escolha de implementação local, não regra de cadastro/publicação documental. Os limites de quantidade/tamanho já existentes não foram alterados.
**Provas reutilizadas.** Data inválida; A/B e nova sessão; recusa de outra conta; histórico24/12 e hashes; resolvedor temporal sintético; controlos financeiros independentes; Word/CSV e respectivos valores; PDF nativo10 páginas/rodapé/última página; inventários/documentos e leitura SSH sem escrita. A Tempo aceitou o fecho do rodapé segundo as provas apresentadas e declarou que não abriu a imagem/PDF pelos caminhos locais. Não lhe atribuir inspecção visual directa. Os testes temporais sintéticos não representam legislação. A prova de recusa do clone com superutilizador é da aplicação; não é uma nova certificação de RLS. Não repetir as provas aceites apenas para gerar uma nova lista de resultados.
**Ensaio dirigido isolado.** `http://127.0.0.1:18576/qa/export-recovery` usa o componente real e os geradores reais, com uma falha única de biblioteca injectada no módulo de ensaio, sem alteração das rotas de produção. Os controlos de falha e os ficheiros sintéticos ficam em `outputs/`, fora do Git e do pacote. Os contadores referem-se a chamadas/generações; para ZIP contam a geração interna de Word e a preparação do arquivo, não duas transferências. No ensaio de nova tentativa de PDF é omitida apenas a impressão automática, para observar a janela com conteúdo e não repetir a prova nativa aceite. Nenhuma dessas injecções entra na candidata funcional.
**Procedimento de integração após revisão.** Fixar o commit candidato e manifesto de fontes; obter o `main` remoto actualizado e reconciliar apenas os deltas dos programadores que afectem estas fontes/consumidores. Rever o diff e conferir no ambiente QA a revisão exacta a integrar; repetir apenas verificações afectadas por conflitos ou alterações posteriores. Este delta não contém migrations, cadastro de regras, importação, publicação de documentos, contas, permissões ou alterações de MFA/configuração. Após decisão da Tempo, integrar pelo processo Git existente; confirmar o recibo da cadeia e a revisão efectivamente servida em `/build/revision.json`, com consulta pública e login privado conferidos nos consumidores afectados. Uma falha deve conservar a release anterior; não editar a pasta servida para contornar a cadeia. A reversão usa o mecanismo de release anterior de código/build, preservando base, anexos, configuração e dados posteriores; não repor um dump antigo. Se uma futura entrega incluir regras/dados activos, exigir o procedimento específico já aprovado de backup/restauro e acção autenticada; esse trabalho não está incluído aqui.
### Diagnóstico fiscal separado da candidata — 21/10/2026
| Camada | Existente e comprovado | Saldo efectivo |
|---|---|---|
| Fontes | Inventário anterior de9901 caminhos e32 candidatos por nome; três originais versionados: BR248/I/Suplemento1/2025, republicação Lei22/2022 no BR250/2.ºSuplemento e BR89/2017. Análises em `primary-text-review-2025-2026.md`, `official-source-register.md`, `fiscal-reform-2026-review.md` e referências em `fiscal-source-references-2026-10-06.csv`. | Reutilizar conteúdo, hashes, versões e citações já disponíveis; não pedir novamente esses documentos. A enumeração de caminhos não equivale à leitura de todos os binários. Distinguir fontes já lidas de fontes só inventariadas e alterações/anexos ainda sem correspondência. |
| Parâmetros executáveis | Leitura administrativa de todos os estados confirmou zero linhas em `simulation_tax_rule_versions` tanto na QA como na homologação. A homologação tem seis referências em `investment_legal_sources`, ligadas a estruturas territoriais; não são taxas/fórmulas prontas. | Cadastrar por componente: código/versão, diploma/artigo/versão/hash, âmbito e condições, início/fim de aplicação, valor/unidade, bases/limites/exclusões, fórmula e arredondamento fundamentados. Os campos necessários variam por regra; não preencher lacunas por zero ou pela taxa mais recente. |
| Governação e consulta | Modelo versionado, controlador de transições, API de resolução por componente/período/contexto, selecção de publicados e compatibilidade de versão da fonte; resolução temporal e indeterminação já ensaiadas. | Reutilizar estes serviços; conferir as correspondências documentais e a operação com parâmetros reais quando estes existirem. Não criar aprovações, nomeações ou uma nova exigência organizacional por escolha técnica. A Tempo define os papéis e as regras. |
| Expressões | `SimulationTaxRuleExpression` já valida e avalia a DSL `investmoz.tax.v1` com aritmética racional, arredondamento e valores em falta. | Não reconstruir o avaliador. Verificar se cada regra fundamentada cabe na linguagem existente e estender apenas operações concretamente necessárias. |
| Execução fiscal integrada | `RunSimulation` executa `base × taxa/100 × (1−redução/100)`, identifica `custom_planning`, `legal_interpretation=false`, `ruleset_version=null` e moeda não indicada nesse cenário. Consulta/DSL não são consumidas por este fluxo como motor fiscal. | Desenvolver orquestração explícita de componente/período/âmbito → regras elegíveis → entradas e parâmetros → avaliação → resultado e explicação com fontes/versões imutáveis. Tratar ambiguidade/transição/ausência como indeterminação. Manter o cenário de planeamento separado, sem o relabelar como imposto calculado. |
| Resultado, dossier e relatório | Snapshots, histórico e exportadores existentes podem transportar resultados e proveniência; relatórios actuais identificam pressupostos/estimativas. | Integrar o resultado fiscal com identificação própria de regra, período, unidade/moeda, base, passos, arredondamento e lacunas. Guardar reprodução histórica e demonstrar casos de referência por regra, isolamento e não reaplicação de uma versão actual a um resultado antigo. |
**Componentes a parametrizar, não taxas inventadas.** O acervo já analisado identifica famílias de alterações para ICE (Lei7/2025/tabela), ISPC (Lei9/2025), IVA (republicação Lei22/2022 e Lei10/2025, além da consolidação/rectificação aplicável), IRPS (Lei11/2025) e IRPC (Lei12/2025). A API também contempla INSS, retenções e direitos aduaneiros. Isto é inventário de domínios, não prova de cobertura documental completa nem autorização para aplicar uma taxa genérica. Para ICE/aduaneiro são relevantes código/unidade/tabela e período; para IVA, operação, incidência/exclusões/dedução e estado de reembolso; para ISPC, elegibilidade/limites e obrigações; para IRPS/IRPC, categoria, residência/estabelecimento, base, retenções e regimes específicos. Prazos, taxas, anexos e incentivos só entram com correspondência documental aplicável.
A aceitação das fontes oficiais determinada pela Tempo em08/10 prevalece sobre pedidos históricos genéricos de reconfirmação de autenticidade ou de um fiscalista adicional. Identificar o artigo, âmbito, período, alterações/revogação e parâmetros é aplicação/rastreabilidade da fonte aceite, não segunda validação da sua credibilidade. A biblioteca documental, a situação jurídica e a utilização em cálculo são estados independentes; importar ou publicar documentos nunca activa cálculo fiscal.
**Desenvolvimento fiscal necessário:** correspondência documental e cadastro versionado; adaptação concreta de expressões quando necessária; orquestração do cálculo; contrato de resultado/proveniência/histórico; ligação à interface/relatórios e casos de referência por componente/período. Nenhuma dessas frentes é encerrada por corrigir crédito, orçamento, tesouraria, falhas de exportação ou rodapé. O esforço integral deve ser dimensionado por regras/componentes e lacunas documentais identificadas; não cabe na estimativa de1–2h do recorte de exportação.
### Fecho do recorte de exportações e saldo da candidata — 21/10/2026
| Item existente | Saldo consolidado da candidata | Prova / limite |
|---|---|---|
| AUD-SIM-INT01 | Concluído no recorte | Validação de data: prova anterior reutilizada. |
| AUD-SIM-INT02 | Parcial; implementação fiscal integral aberta | Zero regras remotas/QA; fontes, cadastro e integração discriminados no diagnóstico separado. |
| AUD-SIM-INT03 | Concluído nas provas delimitadas | A/B/nova sessão e recusas anteriores; tesouraria com API/nova sessão e UI/recarga, sem alegar uma nova prova browser da mesma sessão. Limite RLS mantido. |
| AUD-SIM-INT04 | Concluído no recorte demonstrado | Histórico/snapshots e resolvedor sintético reutilizados; integração real fiscal permanece em02. |
| AUD-SIM-INT05 | Concluído nos controlos delimitados | Controlos independentes e provas financeiras aceites reutilizados; sem validação legal de impostos. |
| AUD-SIM-INT06 | Concluído na apresentação/comparação deste relatório | Rodapé nativo aceito segundo provas; sem repetir10páginas/valores/CSV; sem atribuir inspecção local ao utilizador. |
| AUD-SIM-INT07 | Concluído no recorte dirigido QA |3falhas de geração/retoma, anexo16MB recusado com lista conservada, re-selecção sem duplicação e ZIP íntegro; injecção sintética fora da entrega. |
| AUD-SIM-INT08 | Concluído nos consumidores afectados | Mensagens portuguesas/diagnóstico técnico separado; limites de catálogo/governação mantidos, sem certificação global. |
Esforço restante do recorte de engenharia de exportações:0h; revisão consolidada e eventual integração continuam pendentes. As1–2h anteriores foram estimativa deste recorte; não eram estimativa da implementação fiscal integral. Não fechar a revisão integral do simulador nem AUD-SIM-INT02 por este incremento. Word/PDF/ZIP foram ensaiados no componente real com falhas sintéticas de geração de um invólucro QA privado; ficheiros de67bytes e16MiB seleccionados manualmente pelo utilizador, estados observados directamente pelo Codex. A primeira recusa encontrou lista vazia e não foi usada como prova de preservação; a sequência posterior demonstrou lista válida→recusa16MB mantendo1→re-selecção válida mantendo1. ZIP real verificado por bytes, entrada única, título e secção. Leitura transitória falhada e concorrência demonstradas nos testes dirigidos, não por avaria real do sistema de ficheiros. A limitação anterior da selecção automática não foi contornada: a selecção normal foi manual. Os detalhes técnicos permanecem em diagnóstico local; não foi criado serviço de recolha remoto.
Provas actuais em outputs/simulator-continuation-20261020/: export-word-error-20261021.*, export-pdf-error-20261021.txt, export-pdf-retry-20261021.txt, export-zip-error-20261021.*, export-zip-retry-20261021.txt, attachment-limit-preserved-20261021.*, attachment-duplicate-retry-20261021.*, export-zip-content-proof-20261021.json e export-recovery-candidate-20261021.json. O procedimento de integração e diagnóstico fiscal imediatamente anteriores integram esta mesma candidata e o mesmo registo; sem matriz/backlog adicional.
File diff suppressed because one or more lines are too long
+133 -100
View File
@@ -1,11 +1,15 @@
import { useMemo, useState } from 'react';
import { useMemo, useRef, useState } from 'react';
import {
DossierExportGate,
dossierExportFailure,
prepareDossierAttachments,
} from '@/lib/dossier-export-state';
import {
createProjectDossierPrintHtml,
createProjectDossierReport,
dossierReportSections,
generateProjectDossierDocx,
generateProjectDossierPackage,
validateDossierAttachments,
type DossierAttachment,
type DossierReference,
type DossierCurrentCalculation,
@@ -57,6 +61,8 @@ export function ProjectDossierExport({
const [attachments, setAttachments] = useState<DossierAttachment[]>([]);
const [error, setError] = useState('');
const [busy, setBusy] = useState('');
const [notice, setNotice] = useState('');
const exportGate = useRef(new DossierExportGate());
const selected = useMemo(
() => attachments.filter((item) => item.include),
[attachments],
@@ -66,20 +72,33 @@ export function ProjectDossierExport({
[selectedSections],
);
function addFiles(files: FileList | null): void {
async function addFiles(files: FileList | null): Promise<void> {
if (!files?.length) return;
const incoming = Array.from(files);
const combined = [...attachments.map((item) => item.file), ...incoming];
const problem = validateDossierAttachments(combined);
if (problem) {
setError(problem);
return;
}
setAttachments((current) => [
...current,
...incoming.map((file) => ({ file, include: true, caption: '' })),
]);
setError('');
await exportGate.current.run(async () => {
setBusy('attachments');
setNotice('');
try {
const result = await prepareDossierAttachments(
attachments,
incoming,
);
if (result.problem) {
setError(result.problem);
return;
}
setAttachments(result.attachments);
setError('');
if (result.duplicateCount)
setNotice(
'Os ficheiros que já estavam na lista foram mantidos uma única vez.',
);
} catch (cause) {
setError(dossierExportFailure('attachments', cause));
} finally {
setBusy('');
}
});
}
function toggleSection(id: DossierReportSectionId, checked: boolean): void {
@@ -118,99 +137,103 @@ export function ProjectDossierExport({
}
async function exportWord(): Promise<void> {
setBusy('word');
setError('');
try {
const report = model();
const blob = await generateProjectDossierDocx(
report,
selected,
selectedSections.has('annexes'),
);
download(
blob,
`relatorio-investmoz-${filenameStem(report.title)}.docx`,
);
} catch (cause) {
setError(
cause instanceof Error
? cause.message
: 'Não foi possível gerar o relatório Word.',
);
} finally {
setBusy('');
}
await exportGate.current.run(async () => {
setBusy('word');
setError('');
try {
const report = model();
const blob = await generateProjectDossierDocx(
report,
selected,
selectedSections.has('annexes'),
);
download(
blob,
`relatorio-investmoz-${filenameStem(report.title)}.docx`,
);
} catch (cause) {
setError(dossierExportFailure('word', cause));
} finally {
setBusy('');
}
});
}
async function exportPackage(): Promise<void> {
setBusy('package');
setError('');
try {
const report = model();
const docx = await generateProjectDossierDocx(
report,
selected,
selectedSections.has('annexes'),
);
const archive = await generateProjectDossierPackage(
docx,
selected,
selectedSections.has('annexes'),
);
download(
archive,
`relatorio-investmoz-${filenameStem(report.title)}-anexos.zip`,
);
} catch (cause) {
setError(
cause instanceof Error
? cause.message
: 'Não foi possível preparar o pacote de anexos.',
);
} finally {
setBusy('');
}
await exportGate.current.run(async () => {
setBusy('package');
setError('');
try {
const report = model();
const docx = await generateProjectDossierDocx(
report,
selected,
selectedSections.has('annexes'),
);
const archive = await generateProjectDossierPackage(
docx,
selected,
selectedSections.has('annexes'),
);
download(
archive,
`relatorio-investmoz-${filenameStem(report.title)}-anexos.zip`,
);
} catch (cause) {
setError(dossierExportFailure('package', cause));
} finally {
setBusy('');
}
});
}
async function exportPdf(): Promise<void> {
const printWindow = window.open('about:blank', '_blank');
if (!printWindow) {
setError(
'O browser bloqueou a janela de impressão. Permita pop-ups para abrir a versão PDF.',
);
return;
}
setBusy('pdf');
setError('');
try {
const html = await createProjectDossierPrintHtml(
model(),
selected,
selectedSections.has('annexes'),
);
printWindow.document.open();
printWindow.document.write(html);
printWindow.document.close();
} catch (cause) {
printWindow.close();
setError(
cause instanceof Error
? cause.message
: 'Não foi possível preparar a versão para PDF.',
);
} finally {
setBusy('');
}
await exportGate.current.run(async () => {
const printWindow = window.open('about:blank', '_blank');
if (!printWindow) {
setError(
'O browser bloqueou a janela de impressão. Permita pop-ups para abrir a versão PDF.',
);
return;
}
setBusy('pdf');
setError('');
try {
const html = await createProjectDossierPrintHtml(
model(),
selected,
selectedSections.has('annexes'),
);
printWindow.document.open();
printWindow.document.write(html);
printWindow.document.close();
} catch (cause) {
printWindow.close();
setError(dossierExportFailure('pdf', cause));
} finally {
setBusy('');
}
});
}
function exportCsv(): void {
const blob = new Blob([exportProjectDossierCsv(draft)], {
type: 'text/csv;charset=utf-8',
async function exportCsv(): Promise<void> {
await exportGate.current.run(async () => {
setBusy('csv');
setError('');
try {
const blob = new Blob([exportProjectDossierCsv(draft)], {
type: 'text/csv;charset=utf-8',
});
download(
blob,
`dossier-projecto-investmoz-${filenameStem(String(draft.project_title ?? ''))}.csv`,
);
} catch (cause) {
setError(dossierExportFailure('csv', cause));
} finally {
setBusy('');
}
});
download(
blob,
`dossier-projecto-investmoz-${filenameStem(String(draft.project_title ?? ''))}.csv`,
);
}
const totalBytes = attachments.reduce(
@@ -261,6 +284,7 @@ export function ProjectDossierExport({
>
<input
type="checkbox"
disabled={Boolean(busy)}
checked={selectedSections.has(section.id)}
onChange={(event) =>
toggleSection(
@@ -298,9 +322,10 @@ export function ProjectDossierExport({
id="project-report-files"
type="file"
multiple
disabled={Boolean(busy)}
accept="image/jpeg,image/png,text/csv,text/plain,.csv,.tsv,.txt,.pdf,.doc,.docx,.xlsx,.odt"
onChange={(event) => {
addFiles(event.currentTarget.files);
void addFiles(event.currentTarget.files);
event.currentTarget.value = '';
}}
/>
@@ -322,6 +347,7 @@ export function ProjectDossierExport({
<input
aria-label={`Incluir ${item.file.name}`}
type="checkbox"
disabled={Boolean(busy)}
checked={item.include}
onChange={(event) =>
updateAttachment(index, {
@@ -340,6 +366,7 @@ export function ProjectDossierExport({
<button
type="button"
className="rounded border px-2 py-1 text-sm"
disabled={Boolean(busy)}
onClick={() => removeAttachment(index)}
>
Remover
@@ -352,6 +379,7 @@ export function ProjectDossierExport({
<input
className="rounded border px-3 py-2"
maxLength={300}
disabled={Boolean(busy)}
value={item.caption}
onChange={(event) =>
updateAttachment(index, {
@@ -372,6 +400,11 @@ export function ProjectDossierExport({
em zero. O relatório identifica pressupostos, estimativas e
referências sem as apresentar como resultados confirmados.
</p>
{notice && (
<p role="status" className="text-sm">
{notice}
</p>
)}
{error && (
<p role="alert" className="text-sm text-red-800">
{error}
@@ -414,7 +447,7 @@ export function ProjectDossierExport({
type="button"
className="rounded border px-4 py-2"
disabled={Boolean(busy)}
onClick={exportCsv}
onClick={() => void exportCsv()}
>
Exportar CSV complementar
</button>
+89
View File
@@ -0,0 +1,89 @@
import {
validateDossierAttachments,
type DossierAttachment,
} from './project-dossier-report.ts';
export type DossierExportOperation =
| 'word'
| 'package'
| 'pdf'
| 'csv'
| 'attachments';
const messages: Record<DossierExportOperation, string> = {
word: 'Não foi possível gerar o relatório Word.',
package: 'Não foi possível preparar o pacote de anexos.',
pdf: 'Não foi possível preparar a versão para PDF.',
csv: 'Não foi possível preparar o ficheiro CSV.',
attachments: 'Não foi possível ler os anexos seleccionados.',
};
let failureSequence = 0;
export function dossierExportFailure(
operation: DossierExportOperation,
cause: unknown,
log: (message: string, detail: unknown) => void = console.error,
): string {
const reference = `EXP-${Date.now().toString(36)}-${++failureSequence}`;
// Local diagnostics only: never send dossier values or file contents.
log('[InvesteMoz exportação]', { reference, operation, cause });
return `${messages[operation]} Os dados, as secções e os anexos foram mantidos. Tente novamente pelo mesmo botão. Referência: ${reference}.`;
}
/** Synchronous acquisition prevents a second click before React renders busy. */
export class DossierExportGate {
private active = false;
async run(task: () => Promise<void>): Promise<boolean> {
if (this.active) return false;
this.active = true;
try {
await task();
return true;
} finally {
this.active = false;
}
}
}
export async function prepareDossierAttachments(
current: DossierAttachment[],
incoming: File[],
): Promise<{
attachments: DossierAttachment[];
duplicateCount: number;
problem?: string;
}> {
for (const file of incoming) {
const problem = validateDossierAttachments([file]);
if (problem)
return { attachments: current, duplicateCount: 0, problem };
}
const identity = async (file: File) => {
const hash = await crypto.subtle.digest(
'SHA-256',
await file.arrayBuffer(),
);
return `${file.name}:${Array.from(new Uint8Array(hash), (b) => b.toString(16).padStart(2, '0')).join('')}`;
};
const known = new Set(
await Promise.all(current.map((item) => identity(item.file))),
);
const added: DossierAttachment[] = [];
let duplicateCount = 0;
for (const file of incoming) {
const key = await identity(file);
if (known.has(key)) {
duplicateCount++;
continue;
}
known.add(key);
added.push({ file, include: true, caption: '' });
}
const problem = validateDossierAttachments(
[...current, ...added].map((item) => item.file),
);
return {
attachments: problem ? current : [...current, ...added],
duplicateCount,
problem: problem ?? undefined,
};
}
+6 -1
View File
@@ -659,8 +659,13 @@ export async function createProjectDossierPrintHtml(
content.push('<p>Não há informação fornecida nesta secção.</p>');
content.push('</section>');
}
// Page margin boxes reserve footer space on every printed page. A fixed
// body element can overlap fragmented tables in Chromium's print layout.
const printFooter = JSON.stringify(
`InvesteMoz · Relatório privado · ${model.version}`,
).replaceAll('<', '\\3c ');
return `<!doctype html><html lang="pt-MZ"><head><meta charset="utf-8"><title>Relatório — ${escapeHtml(model.title)}</title><style>
@page{size:A4;margin:18mm 17mm 20mm}*{box-sizing:border-box}body{font:10.5pt/1.45 Arial,sans-serif;color:#17211f;margin:0}h1{font-size:24pt;color:#123b35;margin:0 0 7mm}h2{font-size:16pt;color:#123b35;border-bottom:1px solid #b8c9c3;padding-bottom:2mm;margin:8mm 0 3mm;break-after:avoid}h3{font-size:12pt;color:#27675c;break-after:avoid}p{white-space:pre-wrap}table{width:100%;border-collapse:collapse;table-layout:fixed;margin:3mm 0 5mm;break-inside:auto}th,td{border:1px solid #cbd5d1;padding:5px 6px;text-align:left;vertical-align:top;overflow-wrap:anywhere}th{background:#123b35;color:white}tr{break-inside:avoid}.report-table th:first-child,.report-table td:first-child{width:22%}.report-table th:last-child,.report-table td:last-child{width:32%}.report-table td:last-child{font-size:9pt;color:#40524d}.annex-table{table-layout:fixed;font-size:9pt}thead{display:table-header-group}.private{padding:3mm;background:#fff4df;border-left:3px solid #b66a0c;font-weight:bold}section{break-inside:auto}figure{margin:5mm auto;text-align:center;break-inside:avoid}img{max-width:100%;max-height:210mm;object-fit:contain}figcaption{font-style:italic;color:#52615d}pre{white-space:pre-wrap;overflow-wrap:anywhere;font:9.5pt/1.4 Arial,sans-serif}@media screen{body{max-width:1040px;margin:24px auto;padding:24px}table{font-size:11pt}}@media screen{body{max-width:1040px;margin:24px auto;padding:24px}table{font-size:11pt}}@media print{a{color:inherit;text-decoration:none}.no-print{display:none}th{color:#17211f;background:#edf4f1}footer{position:fixed;bottom:-14mm;left:0;right:0;font-size:9pt;color:#40524d}}
@page{size:A4;margin:18mm 17mm 20mm;@bottom-left{content:${printFooter};font:9pt/1.3 Arial,sans-serif;color:#40524d;vertical-align:middle;width:80%}@bottom-right{content:counter(page) " / " counter(pages);font:9pt/1.3 Arial,sans-serif;color:#40524d;vertical-align:middle}}*{box-sizing:border-box}body{font:10.5pt/1.45 Arial,sans-serif;color:#17211f;margin:0}h1{font-size:24pt;color:#123b35;margin:0 0 7mm}h2{font-size:16pt;color:#123b35;border-bottom:1px solid #b8c9c3;padding-bottom:2mm;margin:8mm 0 3mm;break-after:avoid}h3{font-size:12pt;color:#27675c;break-after:avoid}p{white-space:pre-wrap}table{width:100%;border-collapse:collapse;table-layout:fixed;margin:3mm 0 5mm;break-inside:auto}th,td{border:1px solid #cbd5d1;padding:5px 6px;text-align:left;vertical-align:top;overflow-wrap:anywhere}th{background:#123b35;color:white}tr{break-inside:avoid}.report-table th:first-child,.report-table td:first-child{width:22%}.report-table th:last-child,.report-table td:last-child{width:32%}.report-table td:last-child{font-size:9pt;color:#40524d}.annex-table{table-layout:fixed;font-size:9pt}thead{display:table-header-group}.private{padding:3mm;background:#fff4df;border-left:3px solid #b66a0c;font-weight:bold}section{break-inside:auto}figure{margin:5mm auto;text-align:center;break-inside:avoid}img{max-width:100%;max-height:210mm;object-fit:contain}figcaption{font-style:italic;color:#52615d}pre{white-space:pre-wrap;overflow-wrap:anywhere;font:9.5pt/1.4 Arial,sans-serif}@media screen{body{max-width:1040px;margin:24px auto;padding:24px}table{font-size:11pt}}@media screen{body{max-width:1040px;margin:24px auto;padding:24px}table{font-size:11pt}}@media print{a{color:inherit;text-decoration:none}.no-print{display:none}th{color:#17211f;background:#edf4f1}footer{display:none}}
</style></head><body><h1>InvesteMoz<br>Relatório consolidado do projecto</h1><p><strong>${escapeHtml(model.title)}</strong><br>Gerado em ${escapeHtml(new Date(model.generatedAt).toLocaleString('pt-MZ'))}<br>Versão/revisão: ${escapeHtml(model.version)}</p><p class="private">Documento privado. O portal não publica nem partilha este conteúdo.</p>${content.join('')}<footer><small>InvesteMoz · Relatório privado · ${escapeHtml(model.version)}</small></footer><script>window.addEventListener('load',()=>setTimeout(()=>window.print(),350));</script></body></html>`;
}
@@ -0,0 +1,141 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import {
DossierExportGate,
dossierExportFailure,
prepareDossierAttachments,
} from '../../resources/js/lib/dossier-export-state.ts';
import {
createProjectDossierReport,
generateProjectDossierDocx,
createProjectDossierPrintHtml,
generateProjectDossierPackage,
} from '../../resources/js/lib/project-dossier-report.ts';
test('concurrent export is ignored; failed generation unlocks one retry without mutating inputs', async () => {
const gate = new DossierExportGate();
const draft = {
project_title: 'SINTÉTICO QA',
project_summary: 'Pressupostos declarados; não é resultado fiscal.',
};
const saved = JSON.stringify(draft);
let release;
let starts = 0;
const first = gate.run(async () => {
starts++;
await new Promise((resolve) => {
release = resolve;
});
throw new Error('Library failure');
});
assert.equal(
await gate.run(async () => {
starts++;
}),
false,
);
release();
await assert.rejects(first, /Library failure/);
assert.equal(
await gate.run(async () => {
starts++;
}),
true,
);
assert.equal(starts, 2);
assert.equal(JSON.stringify(draft), saved);
});
test('all operations show Portuguese recovery text while retaining technical details in diagnostics', () => {
for (const operation of ['word', 'package', 'pdf', 'csv', 'attachments']) {
const error = new Error('English internals / private stack');
const logs = [];
const text = dossierExportFailure(operation, error, (...args) =>
logs.push(args),
);
assert.match(text, /Não foi possível/);
assert.match(text, /foram mantidos.*Tente novamente/);
assert.doesNotMatch(text, /English internals|private stack/);
assert.equal(logs.length, 1);
assert.equal(logs[0][1].cause, error);
assert.ok(text.includes(logs[0][1].reference));
}
});
test('attachment read failure and size refusal preserve existing selection; retry deduplicates bytes and keeps captions', async () => {
const file = new File(['SINTÉTICO QA'], 'qa.txt', { type: 'text/plain' });
const current = [{ file, include: false, caption: 'Legenda conservada' }];
const broken = new File(['another'], 'broken.txt');
broken.arrayBuffer = async () => {
throw new Error('OS read failed');
};
await assert.rejects(
prepareDossierAttachments(current, [broken]),
/OS read failed/,
);
assert.equal(current.length, 1);
const tooLarge = new File([new Uint8Array(16 * 1024 * 1024)], 'large.txt');
const refused = await prepareDossierAttachments(current, [tooLarge]);
assert.equal(refused.attachments, current);
assert.match(refused.problem, /15 MB/);
const result = await prepareDossierAttachments(current, [
new File(['SINTÉTICO QA'], 'qa.txt'),
new File(['distinct'], 'qa.txt'),
]);
assert.equal(result.duplicateCount, 1);
assert.equal(result.attachments.length, 2);
assert.equal(result.attachments[0], current[0]);
assert.equal(result.attachments[0].caption, 'Legenda conservada');
assert.equal(result.attachments[0].include, false);
});
test('actual Word PDF and ZIP generators recover after a transient attachment read failure; archive has one original', async () => {
const model = createProjectDossierReport(
{ project_title: 'QA recuperação' },
{ selectedSections: new Set(['summary', 'annexes']) },
);
const file = new File(['Texto QA'], 'qa.txt', { type: 'text/plain' });
const original = file.text.bind(file);
let fail = false;
file.text = async () => {
if (fail) {
fail = false;
throw new Error('Transient attachment read failure');
}
return original();
};
const attachments = [{ file, include: true, caption: '' }];
for (const generate of [
() => generateProjectDossierDocx(model, attachments, true),
() => createProjectDossierPrintHtml(model, attachments, true),
]) {
fail = true;
await assert.rejects(generate, /Transient attachment/);
assert.ok(await generate());
assert.equal(attachments.length, 1);
}
const word = await generateProjectDossierDocx(model, [], false);
let broken = true;
const { DossierExportGate: Gate } =
await import('../../resources/js/lib/dossier-export-state.ts');
const gate = new Gate();
await assert.rejects(
gate.run(async () => {
if (broken) throw new Error('ZIP write fault');
}),
/ZIP write fault/,
);
broken = false;
let zip;
await gate.run(async () => {
zip = await generateProjectDossierPackage(word, attachments, true);
});
const { default: JSZip } = await import('jszip');
const archive = await JSZip.loadAsync(await zip.arrayBuffer());
assert.deepEqual(
Object.keys(archive.files).filter(
(name) => name.startsWith('Anexos/') && !archive.files[name].dir,
),
['Anexos/qa.txt'],
);
});
@@ -295,6 +295,15 @@ void test('print/PDF HTML escapes user content and explains omitted annexes', as
assert.match(pdf, /&lt;script&gt;/);
assert.match(pdf, /não foram seleccionados|secção de anexos foi omitida/);
assert.match(pdf, /@page\{size:A4/);
assert.match(pdf, /@bottom-left\{content:/);
assert.match(pdf, /@bottom-right\{content:counter\(page\)/);
assert.doesNotMatch(pdf, /position:fixed|bottom:-14mm/);
const escapedVersion = await createProjectDossierPrintHtml(
{ ...report, version: 'Revisão "QA" </style><script>alert(2)</script>' },
[],
false,
);
assert.doesNotMatch(escapedVersion, /<script>alert\(2\)<\/script>/);
});
void test('attachment limits block oversized or excessive selections', () => {