fix: reject stale context previews and protect pending edits
This commit is contained in:
@@ -2141,3 +2141,14 @@ Corrigido no componente existente: comparação por estado e identificador docum
|
||||
Dois novos casos falharam contra o código anterior e passaram na candidata. Dez testes frontend dirigidos passaram, cobrindo também troca com o mesmo nome, retirada, nova associação, nome indisponível, histórico sem aplicação, ausência de alterações e preservação da origem. Tipos, lint, formatação e compilação QA passaram. Provas históricas aceites reutilizadas, sem repetição integral. Recibo e hashes no campo `adaptive_document_impact_increment` de `local-validation-20261029.json`.
|
||||
|
||||
**Visual pendente pela equipa:** na candidata e num dossier autorizado, preparar troca/retirada de documento, comparar antes de confirmar e conferir nomes/estado anterior e posterior; confirmar que associação mantida não surge como substituição e que o histórico permanece. Não é necessário confirmar uma alteração real para observar a prévia. Registar revisão, percurso, perfil, esperado/observado e evidência nesta matriz. Nenhuma instalação ou avanço de main; o incremento não encerra o percurso adaptativo transversal.
|
||||
|
||||
|
||||
### 29/10/2026 — comparação de contexto: proteger edições e recusar prévias desactualizadas
|
||||
|
||||
Continuação sobre `a10dd83d`, nos requisitos existentes V2-07/V2-10 e na orientação adaptativa da Tempo de 23/10. Alguns controlos de declarações, adição de oportunidade e substituição explícita continuavam editáveis durante o pedido de comparação. A resposta podia tornar confirmável um plano anterior às escolhas apresentadas; iniciar outra comparação também conservava o plano precedente em caso de falha.
|
||||
|
||||
Corrigido no editor existente: os controlos referidos acompanham o estado ocupado dos restantes; uma nova comparação invalida o plano e corpo de confirmação anteriores; antes de aceitar a resposta compara-se o snapshot do rascunho pedido com o actual. Se diferirem, a resposta não pode ser confirmada e as edições posteriores ficam conservadas, com pedido explícito para comparar de novo. É consistência técnica da confirmação existente, sem nova condição organizacional e sem alteração de backend, schema, permissões ou cálculo.
|
||||
|
||||
Três casos novos falharam contra a revisão anterior e passaram na candidata: bloqueio/recuperação dos controlos durante comparação, recusa de resposta para rascunho alterado preservando a edição, e falha de nova comparação sem reutilização do plano anterior. Treze testes frontend dirigidos passaram. Tipos, lint, formatação e compilação QA passaram; provas históricas aceites reutilizadas, sem repetição integral. Detalhes/hashes em `context_preview_consistency_increment` do recibo existente `local-validation-20261029.json`.
|
||||
|
||||
**Conferência visual pendente pela equipa:** na candidata, comparar dados de um dossier autorizado, conferir bloqueio temporário e reposição dos controlos; preparar nova edição e repetir a comparação, garantindo que só a prévia actual permite guardar. Em falha de comunicação, os valores permanecem e exige-se nova comparação. Não gravar dados sintéticos no activo para este ensaio. Registar revisão, perfil, percurso, esperado/observado e evidência nesta matriz. Sem instalação ou avanço de main; os requisitos transversais continuam parciais.
|
||||
|
||||
@@ -1290,5 +1290,45 @@
|
||||
"preservation": "Existing before/after backend preview and confirmation flow retained; no data writes, schema, permissions, tax rules or backend changes. Historical accepted suites reused.",
|
||||
"visual_acceptance": "Pending team execution and recording against candidate.",
|
||||
"installation": "Not triggered; main and active release unchanged."
|
||||
},
|
||||
"context_preview_consistency_increment": {
|
||||
"recorded_at": "2026-10-29T21:01:36.362775+00:00",
|
||||
"base_commit": "a10dd83d8dda0eab50d1c77ef3920ba9d5daf172",
|
||||
"requirements": [
|
||||
"V2-07",
|
||||
"V2-10",
|
||||
"Tempo adaptive journey decision 2026-10-23: preserve deliberate edits and compare impact before confirmation"
|
||||
],
|
||||
"behavior": [
|
||||
"Opportunity addition, common declarations and explicit replacement checkboxes are disabled while a context operation is busy, consistently with existing preparation controls.",
|
||||
"Starting a new preview clears the preceding plan and confirmation body, so a failed new preview cannot leave an older preview confirmable.",
|
||||
"Before accepting the asynchronous response, compare the current draft snapshot to the requested draft; reject stale responses and preserve later edits with an explicit request to compare again."
|
||||
],
|
||||
"checks": {
|
||||
"new_cases_reproduced_against_previous_revision": 3,
|
||||
"directed_frontend_passed": 13,
|
||||
"new_frontend_cases": 3,
|
||||
"existing_cases_reused": 10,
|
||||
"types": "passed",
|
||||
"lint": "passed",
|
||||
"format": "passed",
|
||||
"qa_build": "passed"
|
||||
},
|
||||
"source_sha256": {
|
||||
"resources/js/pages/investor-path/context.tsx": "41c919e19246d0e913603cbbb747b82d7ed41f0d93caaadd072443704891eb0d",
|
||||
"tests/frontend/investor-usability-components.test.mjs": "21b23496b8bb009cc52e13e0061e32c18338d8a325eb74c8fbf5e865de8db99f"
|
||||
},
|
||||
"evidence": {
|
||||
"outputs/validation-20261029/context-preview-red.log": "6524233d96ce46217274fd174d2764b4c9b9d2289cbdfd0b48413913fcebded8",
|
||||
"outputs/validation-20261029/context-preview-tests.log": "584f31229dca89319d25fe6f981813bed010b621bb28165f11fbc9f9f44b5915",
|
||||
"outputs/validation-20261029/context-preview-types.log": "82962651e47f564de653ad2883f8fd34e7dae4b4468e4ac459cb3331962a3879",
|
||||
"outputs/validation-20261029/context-preview-lint.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
||||
"outputs/validation-20261029/context-preview-format-check.log": "90f26a8036b540babd912c8093386e9f09e41d13b5debbd3ed1a5c6e4bcbd3c6",
|
||||
"outputs/validation-20261029/context-preview-build.log": "4aafb7a9bf6062810cdbbcd000a49bea2043c89540af876ae1b5d77f397c0ad6",
|
||||
"outputs/validation-20261029/context-preview-build-exit.json": "124fd50cb6d223cdd18fedc166d00af7254199b6e6dedf9be82f71db87af6d01"
|
||||
},
|
||||
"preservation": "Frontend request consistency only. No new business conditions, backend/schema/permissions/tax-rule changes or writes to active data. Existing accepted suites reused without full rerun.",
|
||||
"visual_acceptance": "Pending team execution and recording against candidate; mocked asynchronous component tests are not a browser-session acceptance.",
|
||||
"installation": "Not triggered; main and active release unchanged."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7678,3 +7678,14 @@ Corrigido no componente existente: comparação por estado e identificador docum
|
||||
Dois novos casos falharam contra o código anterior e passaram na candidata. Dez testes frontend dirigidos passaram, cobrindo também troca com o mesmo nome, retirada, nova associação, nome indisponível, histórico sem aplicação, ausência de alterações e preservação da origem. Tipos, lint, formatação e compilação QA passaram. Provas históricas aceites reutilizadas, sem repetição integral. Recibo e hashes no campo `adaptive_document_impact_increment` de `local-validation-20261029.json`.
|
||||
|
||||
**Visual pendente pela equipa:** na candidata e num dossier autorizado, preparar troca/retirada de documento, comparar antes de confirmar e conferir nomes/estado anterior e posterior; confirmar que associação mantida não surge como substituição e que o histórico permanece. Não é necessário confirmar uma alteração real para observar a prévia. Registar revisão, percurso, perfil, esperado/observado e evidência nesta matriz. Nenhuma instalação ou avanço de main; o incremento não encerra o percurso adaptativo transversal.
|
||||
|
||||
|
||||
### 29/10/2026 — comparação de contexto: proteger edições e recusar prévias desactualizadas
|
||||
|
||||
Continuação sobre `a10dd83d`, nos requisitos existentes V2-07/V2-10 e na orientação adaptativa da Tempo de 23/10. Alguns controlos de declarações, adição de oportunidade e substituição explícita continuavam editáveis durante o pedido de comparação. A resposta podia tornar confirmável um plano anterior às escolhas apresentadas; iniciar outra comparação também conservava o plano precedente em caso de falha.
|
||||
|
||||
Corrigido no editor existente: os controlos referidos acompanham o estado ocupado dos restantes; uma nova comparação invalida o plano e corpo de confirmação anteriores; antes de aceitar a resposta compara-se o snapshot do rascunho pedido com o actual. Se diferirem, a resposta não pode ser confirmada e as edições posteriores ficam conservadas, com pedido explícito para comparar de novo. É consistência técnica da confirmação existente, sem nova condição organizacional e sem alteração de backend, schema, permissões ou cálculo.
|
||||
|
||||
Três casos novos falharam contra a revisão anterior e passaram na candidata: bloqueio/recuperação dos controlos durante comparação, recusa de resposta para rascunho alterado preservando a edição, e falha de nova comparação sem reutilização do plano anterior. Treze testes frontend dirigidos passaram. Tipos, lint, formatação e compilação QA passaram; provas históricas aceites reutilizadas, sem repetição integral. Detalhes/hashes em `context_preview_consistency_increment` do recibo existente `local-validation-20261029.json`.
|
||||
|
||||
**Conferência visual pendente pela equipa:** na candidata, comparar dados de um dossier autorizado, conferir bloqueio temporário e reposição dos controlos; preparar nova edição e repetir a comparação, garantindo que só a prévia actual permite guardar. Em falha de comunicação, os valores permanecem e exige-se nova comparação. Não gravar dados sintéticos no activo para este ensaio. Registar revisão, perfil, percurso, esperado/observado e evidência nesta matriz. Sem instalação ou avanço de main; os requisitos transversais continuam parciais.
|
||||
|
||||
@@ -372,6 +372,8 @@ export default function ContextEditor({
|
||||
};
|
||||
async function preview() {
|
||||
setBusy(true);
|
||||
setPlan(null);
|
||||
setRequestBody(null);
|
||||
setError('');
|
||||
setMessage('');
|
||||
const body = {
|
||||
@@ -403,6 +405,11 @@ export default function ContextEditor({
|
||||
`/investimentos/${view.case.id}/contexto/previa`,
|
||||
body,
|
||||
);
|
||||
if (latestDraft.current !== draftBody) {
|
||||
throw new Error(
|
||||
'Os dados mudaram durante a comparação. Reveja novamente antes de guardar.',
|
||||
);
|
||||
}
|
||||
setRequestBody(body);
|
||||
setPlan(result);
|
||||
setPreparation(
|
||||
@@ -656,6 +663,7 @@ export default function ContextEditor({
|
||||
<select
|
||||
className={input}
|
||||
value=""
|
||||
disabled={busy}
|
||||
onChange={(e) => {
|
||||
const o = options.opportunities.find(
|
||||
(o) => o.key === e.target.value,
|
||||
@@ -955,6 +963,7 @@ export default function ContextEditor({
|
||||
{Array.isArray(choices) ? (
|
||||
<select
|
||||
className={input}
|
||||
disabled={busy}
|
||||
value={
|
||||
declarations[String(key)] ??
|
||||
view.context.fields[String(key)] ??
|
||||
@@ -978,6 +987,7 @@ export default function ContextEditor({
|
||||
) : (
|
||||
<input
|
||||
className={input}
|
||||
disabled={busy}
|
||||
type={
|
||||
key === 'expected_start_date'
|
||||
? 'date'
|
||||
@@ -1041,6 +1051,7 @@ export default function ContextEditor({
|
||||
<label className="block" key={k}>
|
||||
<input
|
||||
type="checkbox"
|
||||
disabled={busy}
|
||||
checked={replace.includes(k)}
|
||||
onChange={(e) => {
|
||||
setReplace(
|
||||
|
||||
@@ -476,6 +476,198 @@ const preparationMocks = {
|
||||
},
|
||||
'@/pages/investment/shared': { card: 'card', input: 'input' },
|
||||
};
|
||||
function contextPreviewHarness() {
|
||||
const memory = [];
|
||||
let cursor = 0;
|
||||
const hooks = {
|
||||
...React,
|
||||
useState(initial) {
|
||||
const index = cursor++;
|
||||
if (!(index in memory))
|
||||
memory[index] =
|
||||
typeof initial === 'function' ? initial() : initial;
|
||||
return [
|
||||
memory[index],
|
||||
(next) => {
|
||||
memory[index] =
|
||||
typeof next === 'function' ? next(memory[index]) : next;
|
||||
},
|
||||
];
|
||||
},
|
||||
useRef(initial) {
|
||||
const index = cursor++;
|
||||
if (!(index in memory)) memory[index] = { current: initial };
|
||||
return memory[index];
|
||||
},
|
||||
useEffect() {},
|
||||
};
|
||||
const pending = [];
|
||||
const Editor = load('../../resources/js/pages/investor-path/context.tsx', {
|
||||
react: hooks,
|
||||
'@inertiajs/react': { Link: () => null, router: {} },
|
||||
'@/components/navigation-context': {
|
||||
useNavigationContext: () => ({ refresh() {} }),
|
||||
},
|
||||
'@/lib/operation-key': {
|
||||
createOperationKey: () => 'synthetic-operation',
|
||||
},
|
||||
'@/lib/investor-context': {
|
||||
contextFieldNames: {
|
||||
project_title: 'Título',
|
||||
project_summary: 'Resumo',
|
||||
},
|
||||
contextApi: (url, body) =>
|
||||
new Promise((resolve, reject) =>
|
||||
pending.push({ url, body, resolve, reject }),
|
||||
),
|
||||
},
|
||||
'@/pages/investment/shared': {
|
||||
Frame: () => null,
|
||||
card: 'card',
|
||||
input: 'input',
|
||||
button: 'button',
|
||||
},
|
||||
'@/components/investment-complements': {
|
||||
default: () => null,
|
||||
SelectedComplements: () => null,
|
||||
complementInput: (value) => value,
|
||||
},
|
||||
});
|
||||
const props = {
|
||||
initial: {
|
||||
case: { id: 'synthetic', version: 1, title: 'Synthetic' },
|
||||
context: {
|
||||
selection: {},
|
||||
fields: { currency: 'MZN' },
|
||||
revision: 1,
|
||||
},
|
||||
can_write: true,
|
||||
results: [],
|
||||
requests: [],
|
||||
},
|
||||
options: { opportunities: [], sectors: [], parks: [] },
|
||||
source: null,
|
||||
};
|
||||
return {
|
||||
pending,
|
||||
render() {
|
||||
cursor = 0;
|
||||
return Editor(props);
|
||||
},
|
||||
result: {
|
||||
preview_hash: 'synthetic-preview',
|
||||
selection: {},
|
||||
maintained: [],
|
||||
updated: [],
|
||||
dependent_results: [],
|
||||
review_steps: [],
|
||||
requests: [],
|
||||
documents_preserved: 0,
|
||||
},
|
||||
};
|
||||
}
|
||||
const buttonNamed = (tree, label) =>
|
||||
flat(tree).find(
|
||||
(node) => node.type === 'button' && node.props.children === label,
|
||||
);
|
||||
const settlePreview = () => new Promise((resolve) => setImmediate(resolve));
|
||||
void test('context comparison locks declaration and replacement controls until its matching preview is ready', async () => {
|
||||
const harness = contextPreviewHarness();
|
||||
buttonNamed(
|
||||
harness.render(),
|
||||
'Rever dados antes de guardar',
|
||||
).props.onClick();
|
||||
const busy = harness.render();
|
||||
assert.equal(
|
||||
flat(busy)
|
||||
.filter((node) => node.type === 'select')
|
||||
.every((node) => node.props.disabled),
|
||||
true,
|
||||
);
|
||||
assert.equal(
|
||||
flat(busy)
|
||||
.filter(
|
||||
(node) =>
|
||||
node.type === 'input' &&
|
||||
(node.props.maxLength || node.props.type === 'checkbox'),
|
||||
)
|
||||
.every((node) => node.props.disabled),
|
||||
true,
|
||||
);
|
||||
assert.equal(harness.pending[0].body.version, 1);
|
||||
harness.pending[0].resolve(harness.result);
|
||||
await settlePreview();
|
||||
const ready = harness.render();
|
||||
assert.equal(
|
||||
buttonNamed(ready, 'Guardar dados do investimento').props.disabled,
|
||||
false,
|
||||
);
|
||||
assert.equal(
|
||||
flat(ready).find(
|
||||
(node) => node.type === 'input' && node.props.maxLength === 3,
|
||||
).props.disabled,
|
||||
false,
|
||||
);
|
||||
});
|
||||
void test('context comparison rejects a response for a draft changed while the request was pending', async () => {
|
||||
const harness = contextPreviewHarness();
|
||||
buttonNamed(
|
||||
harness.render(),
|
||||
'Rever dados antes de guardar',
|
||||
).props.onClick();
|
||||
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' } });
|
||||
harness.render();
|
||||
harness.pending[0].resolve(harness.result);
|
||||
await settlePreview();
|
||||
const stale = harness.render();
|
||||
assert.equal(
|
||||
buttonNamed(stale, 'Guardar dados do investimento'),
|
||||
undefined,
|
||||
);
|
||||
assert.ok(
|
||||
flat(stale).some(
|
||||
(node) =>
|
||||
node.type === 'p' &&
|
||||
node.props.children ===
|
||||
'Os dados mudaram durante a comparação. Reveja novamente antes de guardar.',
|
||||
),
|
||||
);
|
||||
assert.equal(
|
||||
flat(stale).find(
|
||||
(node) => node.type === 'input' && node.props.maxLength === 3,
|
||||
).props.value,
|
||||
'USD',
|
||||
);
|
||||
});
|
||||
void test('a failed replacement comparison cannot leave the earlier preview confirmable', async () => {
|
||||
const harness = contextPreviewHarness();
|
||||
buttonNamed(
|
||||
harness.render(),
|
||||
'Rever dados antes de guardar',
|
||||
).props.onClick();
|
||||
harness.render();
|
||||
harness.pending[0].resolve(harness.result);
|
||||
await settlePreview();
|
||||
assert.ok(buttonNamed(harness.render(), 'Guardar dados do investimento'));
|
||||
buttonNamed(
|
||||
harness.render(),
|
||||
'Rever dados antes de guardar',
|
||||
).props.onClick();
|
||||
assert.equal(
|
||||
buttonNamed(harness.render(), 'Guardar dados do investimento'),
|
||||
undefined,
|
||||
);
|
||||
harness.pending[1].reject(new Error('Synthetic request failed'));
|
||||
await settlePreview();
|
||||
assert.equal(
|
||||
buttonNamed(harness.render(), 'Guardar dados do investimento'),
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
void test('preparation directs complete context to missing documents without treating association or uncertainty as approval', () => {
|
||||
const Summary = load(
|
||||
'../../resources/js/components/private-investment-preparation.tsx',
|
||||
|
||||
Reference in New Issue
Block a user