fix: preserve transversal regression contracts and lock budget during confirmation
tests / ci (push) Waiting to run
tests / ci (push) Waiting to run
This commit is contained in:
@@ -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',
|
||||
);
|
||||
|
||||
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user