fix: discard obsolete autosaves and bind acknowledgements to draft sequence

This commit is contained in:
2026-10-29 23:10:29 +02:00
parent ed96fd10b9
commit 87b74f0c2a
5 changed files with 171 additions and 8 deletions
@@ -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.
@@ -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."
}
}
@@ -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.
+12 -8
View File
@@ -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);
@@ -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;