fix: fecha follow-ups pendentes do snapshot no Drive (issue #446) - #459
Conversation
… fecha as duas portas da guarda de device_id, corte de gestos por sequência em vez de relógio, e reversão do rollback por copiar-e-renomear Onze itens acumulados na issue #446 (follow-ups de revisão adversarial dos PRs #443/#447/#448/#449/#452), sem repetir os quatro já entregues no #452: - A guarda do próprio device_id em checkout_on_open comparava contra a aritmética `base + 1`, que só cobria o check-in normal. resolve_conflict_keep_local_core publica além disso (`max(base+1, remote+1)`), então um check-in morto nessa porta caía na restauração normal em vez de ser reconhecido — corrigido gravando a sequência PRETENDIDA (`pending_publish_sequence`) antes de qualquer upload e comparando a guarda contra ela, nos dois pontos de entrada (boot e sonda de foco). - O corte da lista de gestos do conflito usava timestamp contra o relógio do OUTRO aparelho, reintroduzindo a dependência que o árbitro do lease existe para evitar. Trocado por uma sequência monotônica gravada por trigger em cada linha do sync_log (sobrevive ao VACUUM INTO do export, ao contrário do rowid implícito) e uma âncora única (`base_sync_log_seq`) capturada sempre que a base local avança. - rollback_to_safeguard sobrescrevia o banco ativo direto; agora copia para um temporário e troca por rename, a mesma garantia que swap_active_db_atomically já usa. - drive_conflict_details_core limpa conflict_pending_since quando descobre que a disputa não se sustenta mais (remoto sumiu, ou não avançou), liberando os gatilhos automáticos sem exigir um reinício. - Ajustes menores: role="status" no aviso de check-out (era role="alert"), comentário de driveCheckoutLabel corrigido para atribuir a invariante ao mecanismo certo, guard de unmount consistente em SnapshotConflictScreen, conflictRemoteDeviceLabel comparando com this_device_id, App.tsx ganhou cobertura de teste para a costura de conflito automático, e a janela esconde imediatamente ao fechar em vez de ficar parada em silêncio por até 5s. Investigado e descartado: o regex de recência num item da baseline visual do conflito é gerado pelo próprio Playwright (reproduzido de forma independente, mesmo padrão presente em dezenas de outras baselines do repo) — não é um relaxamento de asserção sem causa. Closes #446
|
React Doctor found no new issues. 🎉 Reviewed by React Doctor for commit |
…conder gestos na tela de conflito O trigger que atribuía seq lia MAX(seq) diretamente do sync_log a cada insert. Como a tabela tem deleções em produção (diff-delete do re-import, delete manual de transação), apagar as linhas de maior seq derrubava o MAX corrente — o próximo gesto inserido reusava um número já emitido, abaixo da âncora de algum aparelho, e a lista de conflito voltava a omitir gestos reais. A monotonicidade agora mora numa tabela geradora de uma linha só (sync_log_seq, INTEGER PRIMARY KEY AUTOINCREMENT): o watermark real fica em sqlite_sequence, que o SQLite garante nunca reemitir mesmo após DELETE na tabela, e que sobrevive ao VACUUM INTO junto com o resto do snapshot (verificado por teste de round-trip export→restore).
Defeito bloqueado pela revisão adversarial:
|
|
Aviso para quem testou este branch localmente: a migration 20260814000002 foi corrigida no lugar (nunca chegou ao main), então um banco de dev que aplicou a versão anterior recusa abrir com "migration was previously applied but has been modified" — apague o banco local de dev e deixe as migrações recriarem. |
O quê e por quê
A issue #446 virou um container acumulador de follow-ups de revisão adversarial dos PRs anteriores do mecanismo de sync por snapshot no Drive (#443, #447, #448, #449, #452). O PR #452 já entregou os quatro itens originais do corpo da issue; este PR fecha os onze itens que sobraram no comentário mais recente, sem repetir nada já mergeado.
O item de maior risco real (11a): a guarda que reconhece "esse manifest remoto é a minha própria publicação que morreu entre o upload confirmado e a gravação local" comparava contra a aritmética
base_local + 1— uma janela que só cobria o check-in normal.resolve_conflict_keep_local_core(a resolução de conflito mantendo este aparelho) publicamax(base+1, remote+1), que passa debase+1sempre que a disputa já tinha avançado mais de um passo. Um check-in morto nessa porta específica caía na restauração normal (baixava e trocava o banco ativo pelo próprio snapshot antigo, descartando trabalho local feito depois do upload) em vez de ser reconhecido como "sou eu mesmo".Mudanças
pending_publish_sequence, novo campo emsnapshot_state): gravada ANTES de qualquer upload (check-in normal e resolução de conflito), limpa quando a gravação local confirma. A guarda do própriodevice_idemcheckout_on_opene na sonda de foco compara contra este valor, fechando as duas portas.sync_log.seq, coluna monotônica gravada por trigger SQL — nunca o rowid implícito, queVACUUM INTOpode renumerar;snapshot_state.base_sync_log_seqcomo âncora única). O corte antigo comparava otimestampdo OUTRO aparelho contra uma âncora derivada do relógio DESTE — um relógio remoto atrasado podia esconder gestos de fato posteriores à base. Os dois aparelhos eram bytes idênticos no momento do último sync, então a nova âncora tem o MESMO significado nos dois lados, sem depender de qual relógio está certo.rollback_to_safeguardcopia para temporário e troca por rename em vez de sobrescrever o banco ativo direto — a mesma garantia queswap_active_db_atomicallyjá usa, evitando um banco truncado se a cópia falhar no meio (disco cheio, processo morto).conflict_pending_since: quandodrive_conflict_details_coredescobre que a disputa não se sustenta mais (remoto sumiu do Drive, ou não avançou além da base), limpa o flag antes de devolver o erro — sem isso, os gatilhos automáticos ficavam desligados para sempre e a tela de conflito reabria a cada lançamento só para falhar no mesmo fetch.role="status"no aviso de check-out (erarole="alert", interrompia o leitor de tela a cada visita para um estado passivo recuperável); comentário dedriveCheckoutLabelcorrigido para atribuir a invariante ao mecanismo certo (a guarda decheckout_on_open, não o árbitro puro, que ignoradevice_id); guard de unmount consistente nas duas chamadas deloadDetailsemSnapshotConflictScreen;conflictRemoteDeviceLabelcomparando comthis_device_idem vez de cravar "outro aparelho" sempre;App.tsxganhou cobertura de teste para a costura "tela de conflito abre sozinha" (os doisuseEffectque não tinham nenhum teste); a janela esconde imediatamente ao fechar (window.hide()) em vez de ficar parada e visível em silêncio por até 5s enquanto o check-in final tenta a rede.Investigado e descartado: um item da baseline visual da tela de conflito (
snapshot-conflict.aria.yml) usa regex para a recência de um gesto específico, apesar do relógio pinado no teste. Reproduzi a regeneração de forma independente com um fixture diferente e confirmei que o valor é perfeitamente estável (10 leituras reais, mesmo resultado) — a regex vem do próprio gerador de aria snapshot do Playwright, não de uma condição de corrida real; o mesmo padrão aparece em dezenas de outras baselines deste repo para valores que nem variam com o tempo (ex.: valores monetários). Não é um relaxamento de asserção sem causa a corrigir.Como verificar
npm run check— gate completo (format, lint, typecheck, testes, build,cargo test/clippy/rustfmt, privacy scan, comment hygiene, UI audit) — 100% verde, 1397 testes Rust + suíte TS.npx playwright test tests/e2e/snapshot-conflict-visual.spec.ts— baseline visual regenerada (regra 38 do ui-standards: apagar, gravar, verificar 2×) e 100% verde.seq, rollback copiar-e-renomear, auto-cura do conflito) ganhou teste de regressão ANTES/junto da implementação, incluindo o cenário exato "check-in morto além debase+1" reproduzido tanto no boot quanto na sonda de foco.Closes #446