/* Two devices editing the same note without knowing about each other. * * The write that arrives last wins — refusing it would throw away what was just * typed — but the state it buries has to survive as a version, or the other * device's work is simply gone. That is the whole point of the check, so this * verifies it end to end rather than trusting the route in isolation. */ import { api, check, createNote, createNotebook, launch, report, setTitle, typeInEditor, withFreshUser, } from "./lib.mjs"; const browser = await launch(); let session; try { session = await withFreshUser(browser, "conflict"); const p = session.page; await createNotebook(p, "Konflikt Buch"); await createNote(p); await setTitle(p, "Umkämpft"); await typeInEditor(p, "Ursprünglicher Text von Gerät A."); await p.waitForTimeout(2000); const id = (await api(p, "GET", "/notes")).body[0].id; const base = (await api(p, "GET", `/notes/${id}`)).body.updated_at; check(!!base, "the note reports a revision to base an edit on", String(base)); /* Device B saves — it has seen the same revision, so no conflict. */ const first = await api(p, "PUT", `/notes/${id}`, { content: JSON.stringify({ v: 2, doc: { type: "doc", content: [{ type: "paragraph", content: [{ type: "text", text: "Gerät B war hier." }] }] } }), baseUpdatedAt: base, }); check(first.status === 200, "a save on the current revision is accepted", String(first.status)); check(first.body.conflict === false, "and is not reported as a conflict", JSON.stringify(first.body.conflict)); check(!!first.body.updatedAt, "the server answers with the new revision"); /* Device A saves, still believing the note is at the original revision. */ const stale = await api(p, "PUT", `/notes/${id}`, { content: JSON.stringify({ v: 2, doc: { type: "doc", content: [{ type: "paragraph", content: [{ type: "text", text: "Gerät A schreibt spät." }] }] } }), baseUpdatedAt: base, }); check(stale.status === 200, "the late save is still accepted, not rejected", String(stale.status)); check(stale.body.conflict === true, "but it is reported as a conflict"); /* The late write won... */ const now = (await api(p, "GET", `/notes/${id}`)).body; check(now.content.includes("Gerät A schreibt spät"), "the later write is what the note now holds"); /* ...and the buried version is recoverable. */ const versions = (await api(p, "GET", `/notes/${id}/versions`)).body; check( versions.some((v) => v.reason === "conflict"), "a version is kept and labelled as a conflict", JSON.stringify(versions.map((v) => v.reason)) ); const conflictVersion = versions.find((v) => v.reason === "conflict"); const buried = (await api(p, "GET", `/notes/${id}/versions/${conflictVersion.id}`)).body; check( buried.content.includes("Gerät B war hier"), "and it holds exactly the work the late write buried" ); /* A write with no base at all must not be treated as a conflict — that is how * writes that cannot conflict, like starring, reach the server. */ const noBase = await api(p, "PUT", `/notes/${id}`, { starred: true }); check(noBase.body.conflict === false, "a write without a base revision is never a conflict"); } catch (e) { check(false, "suite ran to the end", e.message); } finally { await session?.cleanup(); const code = report(); await browser.close(); process.exit(code); }