feat(sync): seamless conversation sync, git-like version control, and offline remote notification
This commit is contained in:
1 parent
8e150dcacd
commit
0f866b1bbd
6 files changed
+471
-17
No files matched your search
@@ -0,0 +1,111 @@
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import fs from 'node:fs'
|
||||
import os from 'node:os'
|
||||
import path from 'node:path'
|
||||
import http from 'node:http'
|
||||
import { acceptUpgrade, connect, rejectUpgrade } from '../lib/ws.mjs'
|
||||
import { Mux } from '../lib/mux.mjs'
|
||||
import { Store, sha256 } from '../lib/store.mjs'
|
||||
|
||||
const listen = (srv) => new Promise((r) => srv.listen(0, '127.0.0.1', () => r(srv.address().port)))
|
||||
|
||||
async function linkPair({ handlersA = {}, handlersB = {} } = {}) {
|
||||
const srv = http.createServer()
|
||||
let serverMux
|
||||
const ready = new Promise((resolve) => {
|
||||
srv.on('upgrade', (req, socket, head) => {
|
||||
const ws = acceptUpgrade(req, socket, head)
|
||||
serverMux = new Mux(ws, { odd: false, handlers: handlersA })
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
const port = await listen(srv)
|
||||
const ws = await connect(`ws://127.0.0.1:${port}/link`)
|
||||
const clientMux = new Mux(ws, { odd: true, handlers: handlersB })
|
||||
await ready
|
||||
return { srv, port, serverMux, clientMux, close: () => { clientMux.close(); srv.close() } }
|
||||
}
|
||||
|
||||
test('store: session lineage tracking and offline move queue', () => {
|
||||
const tmp = path.join(os.tmpdir(), `dsh-remote-test-${Date.now()}-${Math.random().toString(36).slice(2)}.json`)
|
||||
try {
|
||||
const store = new Store(tmp)
|
||||
// 1. Lineage record
|
||||
const lin = store.recordLineage('sess-1', {
|
||||
originDevice: 'ai-pc',
|
||||
baseSeq: 10,
|
||||
versionToken: sha256('v1'),
|
||||
status: 'active'
|
||||
})
|
||||
assert.equal(lin.originDevice, 'ai-pc')
|
||||
assert.equal(lin.baseSeq, 10)
|
||||
assert.equal(store.sessionLineage()['sess-1'].status, 'active')
|
||||
|
||||
// 2. Queue move notice for offline device
|
||||
store.queueMoveNotice({
|
||||
sessionId: 'sess-1',
|
||||
fromDevice: 'ai-pc',
|
||||
toDevice: 'local-mac',
|
||||
atSeq: 10,
|
||||
versionToken: sha256('v1')
|
||||
})
|
||||
assert.equal(Object.keys(store.pendingMoves()).length, 1)
|
||||
|
||||
// 3. Popping moves for an unrelated device returns empty
|
||||
const unrelated = store.popPendingMovesForDevice('other-pc')
|
||||
assert.equal(unrelated.length, 0)
|
||||
assert.equal(Object.keys(store.pendingMoves()).length, 1)
|
||||
|
||||
// 4. Popping moves for 'ai-pc' returns the notice and clears queue
|
||||
const popped = store.popPendingMovesForDevice('ai-pc')
|
||||
assert.equal(popped.length, 1)
|
||||
assert.equal(popped[0].sessionId, 'sess-1')
|
||||
assert.equal(popped[0].toDevice, 'local-mac')
|
||||
assert.equal(Object.keys(store.pendingMoves()).length, 0)
|
||||
} finally {
|
||||
try { fs.unlinkSync(tmp) } catch {}
|
||||
}
|
||||
})
|
||||
|
||||
test('mux: session move and git-like conflict detection across link', async () => {
|
||||
const recorded = []
|
||||
const handlersServer = {
|
||||
recordSessionMoved: async (p) => {
|
||||
// Simulate Git-like conflict detection:
|
||||
// If local session progressed past atSeq, both diverged!
|
||||
const localLastSeq = p.mockLocalSeq ?? 5
|
||||
const conflict = localLastSeq > p.atSeq
|
||||
recorded.push({ ...p, conflict, localLastSeq })
|
||||
return { ok: true, conflict, localLastSeq }
|
||||
}
|
||||
}
|
||||
|
||||
const p = await linkPair({ handlersA: handlersServer })
|
||||
try {
|
||||
// 1. Clean move (no divergence: localSeq <= atSeq)
|
||||
const cleanRes = await p.clientMux.call('recordSessionMoved', {
|
||||
sessionId: 'sess-abc',
|
||||
movedTo: 'device-b',
|
||||
targetSessionId: 'sess-xyz',
|
||||
atSeq: 5,
|
||||
mockLocalSeq: 5
|
||||
})
|
||||
assert.equal(cleanRes.ok, true)
|
||||
assert.equal(cleanRes.conflict, false)
|
||||
|
||||
// 2. Conflicting move (local progressed while offline: localSeq > atSeq)
|
||||
const conflictRes = await p.clientMux.call('recordSessionMoved', {
|
||||
sessionId: 'sess-diverged',
|
||||
movedTo: 'device-b',
|
||||
targetSessionId: 'sess-xyz2',
|
||||
atSeq: 5,
|
||||
mockLocalSeq: 8
|
||||
})
|
||||
assert.equal(conflictRes.ok, true)
|
||||
assert.equal(conflictRes.conflict, true)
|
||||
assert.equal(conflictRes.localLastSeq, 8)
|
||||
} finally {
|
||||
p.close()
|
||||
}
|
||||
})
|
||||
Reference in new issue
Block a user