From c0a42bba8e8db1d0ba529035ecf3cbe973c2f20b Mon Sep 17 00:00:00 2001 From: Keaton Hoskins Date: Tue, 6 Oct 2026 20:37:02 -0700 Subject: [PATCH 1/2] test(sync): two idle replicas exchange no segments after the first round (T13256) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Spec §3.5 R7-7: after one real write each and a first sync round, four more idle rounds of cloud sync on both devices send nothing and the stream's segment count holds; both stores converge. Mutation-checked: disabling the sealer's apply-echo subtraction makes A send a segment in round 0. Co-Authored-By: Claude Opus 5.5 --- .changeset/t13256-idle-no-pingpong.md | 16 +++++++ .../src/cloud/__tests__/nexus-vault.test.ts | 44 +++++++++++++++++++ 2 files changed, 60 insertions(+) create mode 100644 .changeset/t13256-idle-no-pingpong.md diff --git a/.changeset/t13256-idle-no-pingpong.md b/.changeset/t13256-idle-no-pingpong.md new file mode 100644 index 0000000000..8f1703180f --- /dev/null +++ b/.changeset/t13256-idle-no-pingpong.md @@ -0,0 +1,16 @@ +--- +id: t13256-idle-no-pingpong +tasks: [T13256] +kind: test +summary: "Two idle replicas syncing in a loop exchange no segments after the first round (journal spec §3.5 R7-7)" +--- + +A new two-device test runs on the fake server. Device B joins the journal device A started, and each device makes one real +write. One round of `cleo cloud sync` carries both writes across. Then four more rounds with no writes run on each device. + +- No device sends a segment in those rounds, and the stream's segment count does not move. +- Applying the other device's segment never makes a device seal or send one back: the apply's echo is subtracted (§3.3). +- Both stores end with the same rows. + +This test guards the R7-7 rule for when receive watermarks land: a watermark rides on an ordinary segment and is never sent +alone after a pull. diff --git a/packages/core/src/cloud/__tests__/nexus-vault.test.ts b/packages/core/src/cloud/__tests__/nexus-vault.test.ts index 09b27973f2..96ce889583 100644 --- a/packages/core/src/cloud/__tests__/nexus-vault.test.ts +++ b/packages/core/src/cloud/__tests__/nexus-vault.test.ts @@ -5400,6 +5400,50 @@ describe('sync enable push: the genesis checkpoint (T12343 S4-1b)', () => { }); }); + it('two idle replicas syncing in a loop exchange no segments after the first round (T13256, §3.5 R7-7)', async () => { + const { m: a, dbPath: aDb } = await journalMachine(); + await on(a, () => enableSyncPush(vopts(a, { allowUnreleased: true }))); + await on(a, async () => { + setSyncFlag(await storeOf(aDb), 'sync.pull', true, { allowUnreleased: true }); + }); + const { b, bDb } = await restoredJoiner(); + expect((await on(b, () => enableSyncPush(vopts(b, { allowUnreleased: true })))).status).toBe( + 'joined', + ); + const sync = async (m: Machine) => { + const r = await on(m, () => cloudSync(vopts(m, { scope: 'project', allowUnreleased: true }))); + expect(r.streams[0]?.status, `${m.name}: ${r.streams[0]?.refused}`).toBe('synced'); + return r.streams[0]; + }; + // One real write each, then a first round carries them both ways. + await on(a, async () => { + (await storeOf(aDb)).exec("UPDATE tasks_tasks SET title = 'from A' WHERE id = 'T1'"); + }); + await on(b, async () => { + (await storeOf(bDb)).exec("UPDATE tasks_tasks SET title = 'from B' WHERE id = 'T2'"); + }); + await sync(a); + await sync(b); + await sync(a); + const settled = fake.stream(STREAM).segments.length; + expect(settled).toBe(2); + // Idle from here on: applying the other's segment never makes either side send one back. + for (let round = 0; round < 4; round++) { + const ra = await sync(a); + const rb = await sync(b); + expect(ra?.sent, `round ${round}: A sent`).toBe(0); + expect(rb?.sent, `round ${round}: B sent`).toBe(0); + } + expect(fake.stream(STREAM).segments).toHaveLength(settled); + const titles = "SELECT id, title FROM tasks_tasks WHERE id IN ('T1', 'T2') ORDER BY id"; + const onA = await on(a, async () => (await storeOf(aDb)).prepare(titles).all()); + expect(await on(b, async () => (await storeOf(bDb)).prepare(titles).all())).toEqual(onA); + expect(onA).toEqual([ + { id: 'T1', title: 'from A' }, + { id: 'T2', title: 'from B' }, + ]); + }); + it('a joined store with push off syncs pull-only: synced, the push leg skipped (T13312)', async () => { const { m: a, dbPath: aDb } = await journalMachine(); await on(a, () => enableSyncPush(vopts(a, { allowUnreleased: true }))); From 0f7ec518b4e81e5595f971ffc77f9ba27ddc9c73 Mon Sep 17 00:00:00 2001 From: Keaton Hoskins Date: Tue, 6 Oct 2026 20:52:10 -0700 Subject: [PATCH 2/2] test(sync): idle rounds also seal and receive nothing (T13256 review nit) Co-Authored-By: Claude Opus 5.5 --- packages/core/src/cloud/__tests__/nexus-vault.test.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/core/src/cloud/__tests__/nexus-vault.test.ts b/packages/core/src/cloud/__tests__/nexus-vault.test.ts index 96ce889583..ad434e3ca1 100644 --- a/packages/core/src/cloud/__tests__/nexus-vault.test.ts +++ b/packages/core/src/cloud/__tests__/nexus-vault.test.ts @@ -5431,8 +5431,15 @@ describe('sync enable push: the genesis checkpoint (T12343 S4-1b)', () => { for (let round = 0; round < 4; round++) { const ra = await sync(a); const rb = await sync(b); - expect(ra?.sent, `round ${round}: A sent`).toBe(0); - expect(rb?.sent, `round ${round}: B sent`).toBe(0); + // Nothing sealed (no echo re-sealed and netted away), sent or received. + for (const [who, r] of [ + ['A', ra], + ['B', rb], + ] as const) { + expect(r?.sealed, `round ${round}: ${who} sealed`).toBe(0); + expect(r?.sent, `round ${round}: ${who} sent`).toBe(0); + expect(r?.received, `round ${round}: ${who} received`).toBe(0); + } } expect(fake.stream(STREAM).segments).toHaveLength(settled); const titles = "SELECT id, title FROM tasks_tasks WHERE id IN ('T1', 'T2') ORDER BY id";