fix: preserve transversal regression contracts and lock budget during confirmation
tests / ci (push) Waiting to run

This commit is contained in:
2026-11-15 19:12:36 +02:00
parent fb076c174f
commit bd000d04d2
8 changed files with 57 additions and 23 deletions
@@ -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.
@@ -4,8 +4,10 @@ import CurrencySelect from './currency-select';
export default function InvestmentBudgetRange({
values,
onChange,
disabled = false,
}: {
values: Record<string, string>;
disabled?: boolean;
onChange?: (field: string, value: string) => void;
}) {
if (
@@ -30,6 +32,7 @@ export default function InvestmentBudgetRange({
{index === 0 ? 'Desde' : 'Até'}
<input
className="min-w-0 rounded border p-2"
disabled={disabled}
type="number"
min="0"
step="any"
@@ -42,6 +45,7 @@ export default function InvestmentBudgetRange({
))}
</div>
<CurrencySelect
disabled={disabled}
label="Moeda do orçamento"
value={values.currency ?? ''}
onChange={(value) => onChange('currency', value)}
@@ -1065,6 +1065,7 @@ function ScopedContextEditor({
))}
</section>
<InvestmentBudgetRange
disabled={busy || !view.can_write}
values={{ ...view.context.fields, ...declarations }}
onChange={(field, value) => {
setDeclarations((old) => ({ ...old, [field]: value }));
@@ -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);
}
@@ -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',
);
+1 -1
View File
@@ -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',
);
});
@@ -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);
@@ -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');