Files
lms-fe/scripts/test-agent-session-sync.cjs

140 lines
5.9 KiB
JavaScript

const { test } = require('node:test')
const assert = require('node:assert/strict')
const fs = require('node:fs')
const vm = require('node:vm')
const ts = require('typescript')
const exportsObject = {}
vm.runInNewContext(ts.transpileModule(fs.readFileSync(require.resolve('../src/renderer/src/features/workspace/agentSessionSync.ts'), 'utf8'), {
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 }
}).outputText, { exports: exportsObject, console: { warn() {} } })
const { createAgentSessionSync } = exportsObject
const message = (id, content = id) => ({ id, role: 'assistant', content })
const plain = value => JSON.parse(JSON.stringify(value))
const deferred = () => {
let resolve
const promise = new Promise(done => { resolve = done })
return { promise, resolve }
}
const storage = () => {
const data = new Map()
return {
getItem: key => data.get(key) ?? null,
setItem: (key, value) => data.set(key, value),
removeItem: key => data.delete(key)
}
}
test('saves replies in order and preserves a newer result arriving during an active save', async () => {
const gate = deferred()
const calls = []
const sync = createAgentSessionSync({ storageKey: 'test', save: async batch => {
calls.push(plain(batch))
if (calls.length === 1) await gate.promise
} })
sync.enqueue([message('command', 'running')])
await Promise.resolve()
sync.enqueue([message('command', 'completed'), message('answer')])
assert.equal(calls.length, 1)
gate.resolve()
await sync.flush()
assert.equal(calls.length, 2)
assert.deepEqual(calls[1], [message('command', 'completed'), message('answer')])
})
test('keeps a reply cached before network completion and after failed persistence', async () => {
const disk = storage()
const sync = createAgentSessionSync({ storage: disk, storageKey: 'student-a', save: async () => { throw Error('offline') } })
sync.enqueue([message('answer')])
assert.deepEqual(JSON.parse(disk.getItem('student-a')), [message('answer')])
await sync.flush()
assert.ok(sync.getError())
const batches = []
const reopened = createAgentSessionSync({ storage: disk, storageKey: 'student-a', save: async batch => { batches.push(plain(batch)) } })
assert.deepEqual(plain(reopened.restore([{ id: 'question', role: 'user', content: '?' }])), [
{ id: 'question', role: 'user', content: '?' }, message('answer')
])
await reopened.flush()
assert.deepEqual(batches, [[message('answer')]])
assert.equal(disk.getItem('student-a'), null)
assert.equal(reopened.getError(), null)
})
test('does not overwrite a new reply with an older history response', async () => {
const sync = createAgentSessionSync({ storageKey: 'test', save: async () => {} })
sync.enqueue([message('answer', 'new answer')])
await sync.flush()
assert.deepEqual(plain(sync.restore([message('answer', 'old answer')])), [message('answer', 'new answer')])
})
test('saves only modified messages even when restored history exceeds 100 messages', async () => {
const batches = []
const sync = createAgentSessionSync({ storageKey: 'test', save: async batch => { batches.push(plain(batch)) } })
const history = Array.from({ length: 150 }, (_, index) => message(`message-${index}`))
sync.restore(history)
sync.enqueue([...history, message('new-answer')])
await sync.flush()
assert.deepEqual(batches, [[message('new-answer')]])
})
test('chunks a large backlog below the server message-count limit', async () => {
const batches = []
const sync = createAgentSessionSync({ storageKey: 'test', save: async batch => { batches.push(plain(batch)) } })
sync.enqueue(Array.from({ length: 205 }, (_, index) => message(`message-${index}`)))
await sync.flush()
assert.deepEqual(batches.map(batch => batch.length), [100, 100, 5])
assert.equal(new Set(batches.flat().map(item => item.id)).size, 205)
})
test('leaving the view does not cancel an accepted write', async () => {
const gate = deferred()
const disk = storage()
let saved = false
const sync = createAgentSessionSync({ storage: disk, storageKey: 'test', save: async () => { await gate.promise; saved = true } })
const unsubscribe = sync.subscribe(() => {})
sync.enqueue([message('answer')])
unsubscribe()
gate.resolve()
await sync.flush()
assert.equal(saved, true)
assert.equal(disk.getItem('test'), null)
})
test('archives after outstanding writes complete and clears cached replies', async () => {
const gate = deferred()
const order = []
const disk = storage()
const sync = createAgentSessionSync({ storage: disk, storageKey: 'test', save: async () => { await gate.promise; order.push('saved') } })
sync.enqueue([message('answer')])
const clearing = sync.clear(async () => { order.push('archived') })
gate.resolve()
await clearing
assert.deepEqual(order, ['saved', 'archived'])
assert.deepEqual(plain(sync.restore([])), [])
assert.equal(disk.getItem('test'), null)
})
test('pending replies stay isolated by student and course scope', async () => {
const disk = storage()
const first = createAgentSessionSync({ storage: disk, storageKey: 'student-a:course-1', save: async () => { throw Error('offline') } })
first.enqueue([message('private-answer')])
await first.flush()
const second = createAgentSessionSync({ storage: disk, storageKey: 'student-b:course-1', save: async () => {} })
assert.deepEqual(plain(second.restore([])), [])
})
test('late command updates cannot recreate a conversation while it is being cleared', async () => {
const archive = deferred()
const calls = []
const sync = createAgentSessionSync({ storageKey: 'test', save: async batch => { calls.push(plain(batch)) } })
sync.enqueue([message('answer')])
await sync.flush()
const clearing = sync.clear(() => archive.promise)
sync.enqueue([message('answer', 'late command result')])
archive.resolve()
await clearing
await sync.flush()
assert.equal(calls.length, 1)
assert.deepEqual(plain(sync.restore([])), [])
})