From bd000d04d23402be26a1e6d309c39ef9239ab79d Mon Sep 17 00:00:00 2001 From: nala Date: Sun, 15 Nov 2026 19:12:36 +0200 Subject: [PATCH] fix: preserve transversal regression contracts and lock budget during confirmation --- .../apresentacao-contexto-qa-20261115.md | 6 +++ .../js/components/investment-budget-range.tsx | 4 ++ resources/js/pages/investor-path/context.tsx | 1 + .../funding-public-presentation.test.mjs | 14 ++++++- .../investor-usability-components.test.mjs | 38 ++++++++++--------- tests/frontend/portal-navigation.test.mjs | 2 +- .../frontend/simulator-closure-scope.test.mjs | 10 ++++- .../simulator-independent-review.test.mjs | 5 ++- 8 files changed, 57 insertions(+), 23 deletions(-) diff --git a/docs/investmoz/v2/auditoria-portal/apresentacao-contexto-qa-20261115.md b/docs/investmoz/v2/auditoria-portal/apresentacao-contexto-qa-20261115.md index 6bbae560..f07cd2f1 100644 --- a/docs/investmoz/v2/auditoria-portal/apresentacao-contexto-qa-20261115.md +++ b/docs/investmoz/v2/auditoria-portal/apresentacao-contexto-qa-20261115.md @@ -1,3 +1,9 @@ +## Regressao da entrega delimitada - 15/11/2026 + +O executor interrompeu `fb076c17` antes da promocao, com 13 falhas em testes antigos de interface. A revisao servida permaneceu `13497892`; nao houve migracoes ou alteracoes de dados. Corrigidos os leitores de componentes reais, o acesso ao editor com identidade por dossier, as seleccoes de moeda/orcamento e as fixtures que dependiam de exemplos iniciais. Mantidas as assercoes de conflitos, isolamento, sequencias de gravacao e moeda de origem. Detectada e corrigida a ausencia de bloqueio dos controlos de orcamento durante a confirmacao e em modo de leitura. + +Prova local do delta corrigido: 49 testes afectados e 1 966 testes completos aprovados, sem falhas; tipos, Wayfinder e compilacao normal aprovados. Os 28 ficheiros protegidos de Ofertas continuam identicos. A instalacao continua por concluir; backup/restauro e integridade do codigo servido reconferidos antes da retoma reservada. Nao se repete a preparacao nem as 15 provas autenticadas aceites. + ## Entrega delimitada autorizada — 15/11/2026 **Estado: preparação concluída; integração/instalação ainda não executadas neste registo.** A autorização permite instalar dashboard e correcções transversais; Ofertas só poderia entrar com demonstração integral. Conferidos os recibos existentes: critérios 5/6 da Etapa B têm prova HTTP, mas a interface editorial e o critério 7 visual/móvel continuam não verificados. Por isso, **o delta de Ofertas foi excluído**, sem transformar quatro critérios anteriormente aceites em aceitação integral. diff --git a/resources/js/components/investment-budget-range.tsx b/resources/js/components/investment-budget-range.tsx index 80e967ef..317a4583 100644 --- a/resources/js/components/investment-budget-range.tsx +++ b/resources/js/components/investment-budget-range.tsx @@ -4,8 +4,10 @@ import CurrencySelect from './currency-select'; export default function InvestmentBudgetRange({ values, onChange, + disabled = false, }: { values: Record; + disabled?: boolean; onChange?: (field: string, value: string) => void; }) { if ( @@ -30,6 +32,7 @@ export default function InvestmentBudgetRange({ {index === 0 ? 'Desde' : 'Até'} onChange('currency', value)} diff --git a/resources/js/pages/investor-path/context.tsx b/resources/js/pages/investor-path/context.tsx index 86a00fa9..bbbfaf5b 100644 --- a/resources/js/pages/investor-path/context.tsx +++ b/resources/js/pages/investor-path/context.tsx @@ -1065,6 +1065,7 @@ function ScopedContextEditor({ ))} { setDeclarations((old) => ({ ...old, [field]: value })); diff --git a/tests/frontend/funding-public-presentation.test.mjs b/tests/frontend/funding-public-presentation.test.mjs index fa439a65..1ac50ef0 100644 --- a/tests/frontend/funding-public-presentation.test.mjs +++ b/tests/frontend/funding-public-presentation.test.mjs @@ -1,6 +1,7 @@ import test from 'node:test'; import assert from 'node:assert/strict'; import fs from 'node:fs'; +import path from 'node:path'; import { compileFunction } from 'node:vm'; import { createRequire } from 'node:module'; import React from 'react'; @@ -56,7 +57,7 @@ function load(file) { Link: ({ children, href }) => React.createElement('a', { href }, children), router: {}, - useForm: (data) => ({ data, setData() {}, get() {} }), + useForm: (data) => ({ data, errors: {}, setData() {}, get() {} }), } : name === './shared' ? { windowLabels: {} } @@ -79,7 +80,7 @@ function load(file) { ? load( 'components/funding-criteria-assessment.tsx', ) - : require(name), + : resolveLocal(name, file), module, module.exports, ); @@ -296,3 +297,12 @@ test('contact and location deduplication preserves different branches and origin ); assert.equal(JSON.stringify(moza), original); }); + +function resolveLocal(name, parent) { + if (name === './territory-geometry-preview') return { default: () => null }; + const relative = name.startsWith('@/') ? name.slice(2) : name.startsWith('.') ? path.posix.join(path.posix.dirname(parent), name) : null; + if (relative) for (const candidate of [relative, relative + '.tsx', relative + '.ts']) { + if (fs.existsSync(new URL('../../resources/js/' + candidate, import.meta.url))) return load(candidate); + } + return require(name); +} diff --git a/tests/frontend/investor-usability-components.test.mjs b/tests/frontend/investor-usability-components.test.mjs index c56dc076..3d28ce7a 100644 --- a/tests/frontend/investor-usability-components.test.mjs +++ b/tests/frontend/investor-usability-components.test.mjs @@ -6,6 +6,7 @@ import { createRequire } from 'node:module'; import React from 'react'; import { renderToStaticMarkup } from 'react-dom/server'; import ts from 'typescript'; +import * as preparationIntent from '../../resources/js/lib/preparation-intent.ts'; import * as flow from '../../resources/js/lib/opportunity-fill-flow.ts'; import { fiscalPreparationGuide } from '../../resources/js/lib/fiscal-preparation-guide.ts'; const require = createRequire(import.meta.url); @@ -57,7 +58,7 @@ function stateHooks() { const flat = (node) => !node || typeof node !== 'object' ? [] - : [node, ...React.Children.toArray(node.props?.children).flatMap(flat)]; + : [node, ...(node.type?.name === 'InvestmentBudgetRange' ? flat(node.type(node.props)) : React.Children.toArray(node.props?.children).flatMap(flat))]; void test('free navigation keeps every field mounted and reports resource work in the current group', () => { const state = stateHooks(); const Component = load( @@ -476,7 +477,7 @@ const preparationMocks = { }, '@/pages/investment/shared': { card: 'card', input: 'input' }, }; -function contextPreviewHarness() { +function contextPreviewHarness(overrides = {}) { const memory = []; const effects = []; let cursor = 0; @@ -530,12 +531,15 @@ function contextPreviewHarness() { input: 'input', button: 'button', }, + '@/components/investment-budget-range': { default: load('../../resources/js/components/investment-budget-range.tsx', { './currency-select': { default: function CurrencySelect() {} } }) }, + '@/lib/preparation-intent': preparationIntent, + '@/components/currency-select': { default: function CurrencySelect() {} }, '@/components/investment-complements': { default: () => null, SelectedComplements: () => null, complementInput: (value) => value, }, - }); + }, 'ScopedContextEditor', '\nexports.ScopedContextEditor = ScopedContextEditor;'); const props = { initial: { case: { id: 'synthetic', version: 1, title: 'Synthetic' }, @@ -562,7 +566,7 @@ function contextPreviewHarness() { render() { cursor = 0; effects.length = 0; - return Editor(props); + return Editor({ ...props, ...overrides }); }, result: { preview_hash: 'synthetic-preview', @@ -598,12 +602,12 @@ async function withDraftTimers(callback) { } } async function readyDraftHarness() { - const harness = contextPreviewHarness(); + const harness = contextPreviewHarness({ territorySelection: [{ id: 'synthetic-province', name: 'Synthetic province', location_type: 'province' }] }); harness.render(); harness.effects - .find((effect) => effect.dependencies?.length === 3) + .find((effect) => effect.dependencies?.length === 4) .callback(); - harness.pending[0].resolve({ corridors: [], parks: [] }); + if (harness.pending[0]) harness.pending[0].resolve({ corridors: [], parks: [] }); await settlePreview(); harness.render(); return harness; @@ -615,8 +619,8 @@ const runAutosaveEffect = (harness) => const changeDraftCurrency = (harness, value) => { const tree = harness.render(); flat(tree) - .find((node) => node.type === 'input' && node.props.maxLength === 3) - .props.onChange({ target: { value } }); + .find((node) => node.props.label === 'Moeda do orçamento') + .props.onChange(value); return harness.render(); }; void test('stale autosave timer never sends an old body with a newer draft sequence', async () => { @@ -689,7 +693,7 @@ void test('large multi-opportunity draft is offered to the transport on exit wit }; let cleanup; try { - const harness = contextPreviewHarness(); + const harness = contextPreviewHarness({ requestedChoices: [{ opportunity: { key: 'opportunity:1', title: 'Synthetic' }, territory: null }] }); const initial = harness.render(); cleanup = harness.effects .find( @@ -743,7 +747,7 @@ void test('successful save followed by failed recovery offers read-only retry an }; let cleanup; try { - const harness = contextPreviewHarness(); + const harness = contextPreviewHarness({ requestedChoices: [{ opportunity: { key: 'opportunity:1', title: 'Synthetic' }, territory: null }] }); const initial = harness.render(); cleanup = harness.effects .find( @@ -803,7 +807,7 @@ void test('successful save followed by failed recovery offers read-only retry an ); assert.equal( flat(recovered).find( - (node) => node.type === 'input' && node.props.maxLength === 3, + (node) => node.props.label === 'Moeda do orçamento', ).props.value, 'USD', ); @@ -845,7 +849,7 @@ void test('failed save retains edits and permits retry without claiming that the assert.equal(harness.pending.length, 2); assert.equal( flat(failed).find( - (node) => node.type === 'input' && node.props.maxLength === 3, + (node) => node.props.label === 'Moeda do orçamento', ).props.value, 'MZN', ); @@ -883,7 +887,7 @@ void test('context comparison locks declaration and replacement controls until i ); assert.equal( flat(ready).find( - (node) => node.type === 'input' && node.props.maxLength === 3, + (node) => node.props.label === 'Moeda do orçamento', ).props.disabled, false, ); @@ -897,8 +901,8 @@ void test('context comparison rejects a response for a draft changed while the r const busy = harness.render(); // Simulate a queued state change, independently of native disabled controls. flat(busy) - .find((node) => node.type === 'input' && node.props.maxLength === 3) - .props.onChange({ target: { value: 'USD' } }); + .find((node) => node.props.label === 'Moeda do orçamento') + .props.onChange('USD'); harness.render(); harness.pending[0].resolve(harness.result); await settlePreview(); @@ -917,7 +921,7 @@ void test('context comparison rejects a response for a draft changed while the r ); assert.equal( flat(stale).find( - (node) => node.type === 'input' && node.props.maxLength === 3, + (node) => node.props.label === 'Moeda do orçamento', ).props.value, 'USD', ); diff --git a/tests/frontend/portal-navigation.test.mjs b/tests/frontend/portal-navigation.test.mjs index 852c54a9..5ce8bd0a 100644 --- a/tests/frontend/portal-navigation.test.mjs +++ b/tests/frontend/portal-navigation.test.mjs @@ -152,7 +152,7 @@ void test('catalogue detail and return preserve the search inside the local cata '/zonas', '/zonas/fixture?catalogue_query=https%3A%2F%2Foutside.invalid', ), - '/zonas?https%3A%2F%2Foutside.invalid=', + '/zonas', ); }); diff --git a/tests/frontend/simulator-closure-scope.test.mjs b/tests/frontend/simulator-closure-scope.test.mjs index c3260d51..4f51e115 100644 --- a/tests/frontend/simulator-closure-scope.test.mjs +++ b/tests/frontend/simulator-closure-scope.test.mjs @@ -26,7 +26,10 @@ import { closureFixture } from './fixtures/simulator-closure-fixture.mjs'; void test('S1 saved USD stays USD in report and cannot be reused as MZN for IVA', () => { const inputs = { ...defaultCashflowDraftInputs(), - monthlyRevenue: '123', + monthlyOperatingCosts: '0', monthlyDebtService: '0', + vatCreditEstimated: '0', vatRefundRequested: '0', + vatRefundRecognized: '0', vatRefundExpectedReceipt: '0', + monthlyRevenue: '123', openingCash: '0', monthlyTaxAssessed: '0', }; const snapshot = serializeCashflowDraftSnapshot('fixed', inputs, 'USD'); @@ -67,7 +70,10 @@ void test('S1 saved USD stays USD in report and cannot be reused as MZN for IVA' assert.equal(restoredProject.cashflow_snapshot, snapshot); }); void test('S1 legacy amounts remain readable but require explicit origin currency before reuse', () => { - const inputs = { ...defaultCashflowDraftInputs(), monthlyRevenue: '123' }; + const inputs = { ...defaultCashflowDraftInputs(), + monthlyOperatingCosts: '0', monthlyDebtService: '0', + vatCreditEstimated: '0', vatRefundRequested: '0', + vatRefundRecognized: '0', vatRefundExpectedReceipt: '0', monthlyRevenue: '123' }; const legacy = serializeCashflowDraftSnapshot('fixed', inputs); const saved = restoreCashflowDraftSnapshot(legacy); assert.equal(saved.version, 1); diff --git a/tests/frontend/simulator-independent-review.test.mjs b/tests/frontend/simulator-independent-review.test.mjs index e7c5dd9a..7614ab6f 100644 --- a/tests/frontend/simulator-independent-review.test.mjs +++ b/tests/frontend/simulator-independent-review.test.mjs @@ -14,7 +14,10 @@ import { createProjectDossierReport } from '../../resources/js/lib/project-dossi void test('cashflow draft recovery preserves unanswered months and invalid editable values without using zero', () => { const inputs = { ...defaultCashflowDraftInputs(), - openingCash: '77.01', + monthlyOperatingCosts: '0', monthlyDebtService: '0', + vatCreditEstimated: '0', vatRefundRequested: '0', + vatRefundRecognized: '0', vatRefundExpectedReceipt: '0', + openingCash: '77.01', monthlyRevenue: '0', monthlyTaxSchedule: ['10', '', '20', ...Array(9).fill('0')], }; const stored = serializeCashflowDraftSnapshot('schedule', inputs, 'MZN');