diff --git a/docs/investmoz/consolidated-review-matrix-2026-10-03.md b/docs/investmoz/consolidated-review-matrix-2026-10-03.md index 0919c29c..3d2281f9 100644 --- a/docs/investmoz/consolidated-review-matrix-2026-10-03.md +++ b/docs/investmoz/consolidated-review-matrix-2026-10-03.md @@ -2174,3 +2174,14 @@ O transporte existente serializa uma vez, conserva o corpo integral e decide por Cinco testes novos e quinze existentes passaram, total de vinte testes frontend dirigidos. Cobertura: corpo Unicode integral, caracteres versus bytes, concorrência/libertação de reserva, falha sem repetição de escrita, GET/POST/CSRF/conflito e rascunho de cem oportunidades entregue integralmente ao transporte ao sair. Tipos, lint, formatação e compilação QA passaram; provas históricas aceites reutilizadas. Recibo/hashes em `context_draft_transport_increment` de `local-validation-20261029.json`. **Limite da prova:** fetch normal após a saída continua a ser tentativa de melhor esforço; o orçamento local não reserva capacidade usada por outro código no navegador. Não se declara gravação garantida ao fechar imediatamente a página. Mantêm-se o envio automático normal, o estado do rascunho e a tentativa explícita após falha. Conferência visual pela equipa permanece pendente: na candidata QA, preparar texto acentuado e selecção extensa, aguardar «Rascunho guardado», sair/regressar e conferir valores e oportunidades recuperados. Registar revisão, perfil, percurso e evidência nesta matriz. Sem instalação, avanço de main ou escrita no activo; backend, schema, permissões e cálculo preservados. + + +### 29/10/2026 — envio automático: sequência própria e respostas de rascunhos antigos + +Continuação sobre `ed96fd10`, nos requisitos existentes V2-07/V2-10 e na orientação adaptativa da Tempo de 23/10. O temporizador obtinha a sequência quando disparava, podendo emparelhar um corpo antigo com uma sequência posterior. Um corpo já ultrapassado também podia continuar na fila; a comparação apenas pelo conteúdo permitia assinalar como guardada uma revisão posterior que tivesse voltado aos mesmos valores. + +O editor captura corpo e sequência em conjunto. Antes do envio em fila e da apresentação de sucesso/erro, exige correspondência do corpo e da sequência actuais, sem recuperação de contexto guardado pendente. Os envios ultrapassados são descartados sem pedido nem indicação de gravação; o envio da revisão actual mantém a sequência própria. A protecção existente do servidor contra versões/sequências antigas foi consultada e preservada, não relaxada. Nenhuma mudança de backend, schema, permissões ou cálculo. + +Dois casos novos falharam contra o código anterior e passaram na candidata. Vinte e dois testes frontend dirigidos passaram: temporizador antigo após edição, envio ultrapassado em fila, resposta tardia depois de A→B→A, sequência crescente e confirmação somente do rascunho actual. Tipos, lint, formatação e compilação QA passaram; provas históricas aceites reutilizadas, sem repetição integral. Recibo/hashes em `context_autosave_ordering_increment` de `local-validation-20261029.json`. + +**Visual pendente pela equipa:** em QA, editar sucessivamente enquanto um envio automático aguarda, incluindo regressar ao valor inicial; aguardar a confirmação da revisão actual, sair/regressar e conferir os últimos valores recuperados. Usar atraso controlado de rede em QA, sem alterações sintéticas no activo. Registar revisão, perfil, percurso, esperado/observado e evidência nesta matriz. A prova com temporizadores/fila não substitui a sessão no navegador. Sem instalação ou avanço de main; requisitos transversais permanecem parciais. diff --git a/docs/investmoz/unification/local-validation-20261029.json b/docs/investmoz/unification/local-validation-20261029.json index a4ed9b79..f8e84b81 100644 --- a/docs/investmoz/unification/local-validation-20261029.json +++ b/docs/investmoz/unification/local-validation-20261029.json @@ -1434,5 +1434,45 @@ "preservation": "No payload truncation, field/rule restrictions, backend/schema/permissions/tax changes or active writes. Authentication, methods and HTTP conflict behavior preserved. Historical accepted suites reused.", "visual_acceptance": "Pending team execution and recording; mocked transport/component tests do not constitute browser acceptance.", "installation": "Not triggered; main and active release unchanged." + }, + "context_autosave_ordering_increment": { + "recorded_at": "2026-10-29T21:10:29.322457+00:00", + "base_commit": "ed96fd10b98041adf549383f98d8ee45449fc831", + "requirements": [ + "V2-07", + "V2-10", + "Tempo adaptive journey decision 2026-10-23: retain choices and deliberate edits across navigation/refresh" + ], + "defect": "Autosave captured the sequence when its timer ran, not when its body was scheduled; queued obsolete bodies could still be sent and a late completion could acknowledge later edits that returned to the same values.", + "behavior": "Capture the body/sequence pair together. Before queued transport and before reporting success/error, require matching current body and sequence and no saved-context recovery pending. Skip obsolete queued drafts without sending them or claiming they were saved.", + "checks": { + "previous_revision_reproduced": { + "passed": 0, + "failed": 2 + }, + "directed_frontend_passed": 22, + "new_frontend_cases": 2, + "existing_cases_reused": 20, + "types": "passed", + "lint": "passed", + "format": "passed", + "qa_build": "passed" + }, + "source_sha256": { + "resources/js/pages/investor-path/context.tsx": "bf8c9201796ffb30edfef8154fa4a6798adae8cada61318002febd5f8e358527", + "tests/frontend/investor-usability-components.test.mjs": "73eba025623874af5cabe1357fea27603ca108b87121ec95e659f4684508ba14" + }, + "evidence": { + "outputs/validation-20261029/context-autosave-red.log": "133130d478c25a34b1227de390fb9011cd924c52cee0876cfc13c3a5c0c7f21d", + "outputs/validation-20261029/context-autosave-tests.log": "2a3bea954316bee825ce3897067158532d3c90ab2435b94d3e47c2cb5fb7cba8", + "outputs/validation-20261029/context-autosave-types.log": "82962651e47f564de653ad2883f8fd34e7dae4b4468e4ac459cb3331962a3879", + "outputs/validation-20261029/context-autosave-lint.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "outputs/validation-20261029/context-autosave-format-check.log": "17d422f6370473903ddc578b4b3975c782e5d063a505553d9e3f046eaff248c3", + "outputs/validation-20261029/context-autosave-build.log": "5a70aa2d82a1b84f5f10f0169e7f3ed523371436fcd1dcc2282a380603711052", + "outputs/validation-20261029/context-autosave-build-exit.json": "124fd50cb6d223cdd18fedc166d00af7254199b6e6dedf9be82f71db87af6d01" + }, + "preservation": "Frontend sequencing only. Existing backend version/sequence conflict checks preserved, not disabled. No schema, data, permissions or tax changes and no active writes. Historical suites reused.", + "visual_acceptance": "Pending team execution and recording; controlled timer/queue tests do not establish browser-session acceptance.", + "installation": "Not triggered; main and active release unchanged." } } diff --git a/docs/investmoz/unification/traceability-and-manual.md b/docs/investmoz/unification/traceability-and-manual.md index 7d34db35..1085f086 100644 --- a/docs/investmoz/unification/traceability-and-manual.md +++ b/docs/investmoz/unification/traceability-and-manual.md @@ -7711,3 +7711,14 @@ O transporte existente serializa uma vez, conserva o corpo integral e decide por Cinco testes novos e quinze existentes passaram, total de vinte testes frontend dirigidos. Cobertura: corpo Unicode integral, caracteres versus bytes, concorrência/libertação de reserva, falha sem repetição de escrita, GET/POST/CSRF/conflito e rascunho de cem oportunidades entregue integralmente ao transporte ao sair. Tipos, lint, formatação e compilação QA passaram; provas históricas aceites reutilizadas. Recibo/hashes em `context_draft_transport_increment` de `local-validation-20261029.json`. **Limite da prova:** fetch normal após a saída continua a ser tentativa de melhor esforço; o orçamento local não reserva capacidade usada por outro código no navegador. Não se declara gravação garantida ao fechar imediatamente a página. Mantêm-se o envio automático normal, o estado do rascunho e a tentativa explícita após falha. Conferência visual pela equipa permanece pendente: na candidata QA, preparar texto acentuado e selecção extensa, aguardar «Rascunho guardado», sair/regressar e conferir valores e oportunidades recuperados. Registar revisão, perfil, percurso e evidência nesta matriz. Sem instalação, avanço de main ou escrita no activo; backend, schema, permissões e cálculo preservados. + + +### 29/10/2026 — envio automático: sequência própria e respostas de rascunhos antigos + +Continuação sobre `ed96fd10`, nos requisitos existentes V2-07/V2-10 e na orientação adaptativa da Tempo de 23/10. O temporizador obtinha a sequência quando disparava, podendo emparelhar um corpo antigo com uma sequência posterior. Um corpo já ultrapassado também podia continuar na fila; a comparação apenas pelo conteúdo permitia assinalar como guardada uma revisão posterior que tivesse voltado aos mesmos valores. + +O editor captura corpo e sequência em conjunto. Antes do envio em fila e da apresentação de sucesso/erro, exige correspondência do corpo e da sequência actuais, sem recuperação de contexto guardado pendente. Os envios ultrapassados são descartados sem pedido nem indicação de gravação; o envio da revisão actual mantém a sequência própria. A protecção existente do servidor contra versões/sequências antigas foi consultada e preservada, não relaxada. Nenhuma mudança de backend, schema, permissões ou cálculo. + +Dois casos novos falharam contra o código anterior e passaram na candidata. Vinte e dois testes frontend dirigidos passaram: temporizador antigo após edição, envio ultrapassado em fila, resposta tardia depois de A→B→A, sequência crescente e confirmação somente do rascunho actual. Tipos, lint, formatação e compilação QA passaram; provas históricas aceites reutilizadas, sem repetição integral. Recibo/hashes em `context_autosave_ordering_increment` de `local-validation-20261029.json`. + +**Visual pendente pela equipa:** em QA, editar sucessivamente enquanto um envio automático aguarda, incluindo regressar ao valor inicial; aguardar a confirmação da revisão actual, sair/regressar e conferir os últimos valores recuperados. Usar atraso controlado de rede em QA, sem alterações sintéticas no activo. Registar revisão, perfil, percurso, esperado/observado e evidência nesta matriz. A prova com temporizadores/fila não substitui a sessão no navegador. Sem instalação ou avanço de main; requisitos transversais permanecem parciais. diff --git a/resources/js/pages/investor-path/context.tsx b/resources/js/pages/investor-path/context.tsx index 3a47d263..0e08ad47 100644 --- a/resources/js/pages/investor-path/context.tsx +++ b/resources/js/pages/investor-path/context.tsx @@ -287,12 +287,17 @@ export default function ContextEditor({ } if (draftBody === confirmedDraftBody.current) return; setDraftStatus('A guardar rascunho…'); + const savedSequence = sequence.current.value; + const isCurrentDraft = () => + latestDraft.current === draftBody && + sequence.current.value === savedSequence && + !awaitingSavedContext.current; const timer = setTimeout(() => { - const savedSequence = sequence.current.value; queue.current = queue.current .catch(() => {}) - .then(() => - contextApi( + .then(() => { + if (!isCurrentDraft()) return; + return contextApi( '/investimentos/' + view.case.id + '/contexto/rascunho', { ...JSON.parse(draftBody), @@ -300,14 +305,13 @@ export default function ContextEditor({ }, 'POST', true, - ), - ) + ); + }) .then(() => { - if (latestDraft.current === draftBody) - setDraftStatus('Rascunho guardado.'); + if (isCurrentDraft()) setDraftStatus('Rascunho guardado.'); }) .catch((e) => { - if (latestDraft.current === draftBody) + if (isCurrentDraft()) setDraftStatus('Rascunho não guardado: ' + e.message); }); }, 400); diff --git a/tests/frontend/investor-usability-components.test.mjs b/tests/frontend/investor-usability-components.test.mjs index 13ddf999..c56dc076 100644 --- a/tests/frontend/investor-usability-components.test.mjs +++ b/tests/frontend/investor-usability-components.test.mjs @@ -581,6 +581,103 @@ const buttonNamed = (tree, label) => (node) => node.type === 'button' && node.props.children === label, ); const settlePreview = () => new Promise((resolve) => setImmediate(resolve)); +async function withDraftTimers(callback) { + const previousSetTimeout = globalThis.setTimeout; + const previousClearTimeout = globalThis.clearTimeout; + const timers = []; + globalThis.setTimeout = (callback) => { + timers.push(callback); + return timers.length; + }; + globalThis.clearTimeout = () => {}; + try { + await callback(timers); + } finally { + globalThis.setTimeout = previousSetTimeout; + globalThis.clearTimeout = previousClearTimeout; + } +} +async function readyDraftHarness() { + const harness = contextPreviewHarness(); + harness.render(); + harness.effects + .find((effect) => effect.dependencies?.length === 3) + .callback(); + harness.pending[0].resolve({ corridors: [], parks: [] }); + await settlePreview(); + harness.render(); + return harness; +} +const runAutosaveEffect = (harness) => + harness.effects + .find((effect) => effect.dependencies?.length === 5) + .callback(); +const changeDraftCurrency = (harness, value) => { + const tree = harness.render(); + flat(tree) + .find((node) => node.type === 'input' && node.props.maxLength === 3) + .props.onChange({ target: { value } }); + return harness.render(); +}; +void test('stale autosave timer never sends an old body with a newer draft sequence', async () => { + await withDraftTimers(async (timers) => { + const harness = await readyDraftHarness(); + runAutosaveEffect(harness); + changeDraftCurrency(harness, 'USD'); + timers[0](); + await settlePreview(); + assert.equal(harness.pending.length, 1); + runAutosaveEffect(harness); + timers[1](); + await settlePreview(); + assert.equal(harness.pending.length, 2); + assert.equal(harness.pending[1].body.declarations.currency, 'USD'); + assert.ok(harness.pending[1].body.draft_sequence > 0); + harness.pending[1].resolve({ saved: true }); + await settlePreview(); + assert.ok( + flat(harness.render()).some( + (node) => node.props.children === 'Rascunho guardado.', + ), + ); + }); +}); +void test('queued obsolete draft and late completion cannot acknowledge a later revision with identical values', async () => { + await withDraftTimers(async (timers) => { + const harness = await readyDraftHarness(); + changeDraftCurrency(harness, 'EUR'); + runAutosaveEffect(harness); + timers[0](); + await settlePreview(); + const firstSequence = harness.pending[1].body.draft_sequence; + changeDraftCurrency(harness, 'USD'); + runAutosaveEffect(harness); + timers[1](); + await settlePreview(); + changeDraftCurrency(harness, 'EUR'); + harness.pending[1].resolve({ saved: true }); + await settlePreview(); + assert.equal(harness.pending.length, 2); + assert.equal( + flat(harness.render()).some( + (node) => node.props.children === 'Rascunho guardado.', + ), + false, + ); + runAutosaveEffect(harness); + timers[2](); + await settlePreview(); + assert.equal(harness.pending[2].body.declarations.currency, 'EUR'); + assert.ok(harness.pending[2].body.draft_sequence > firstSequence); + harness.pending[2].resolve({ saved: true }); + await settlePreview(); + assert.ok( + flat(harness.render()).some( + (node) => node.props.children === 'Rascunho guardado.', + ), + ); + }); +}); void test('large multi-opportunity draft is offered to the transport on exit without truncation', () => { const previousWindow = globalThis.window; let flush;