fix: discard obsolete autosaves and bind acknowledgements to draft sequence
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user