feat(ai): improve agent workflows and persistence for v1.2.14
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
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([])), [])
|
||||
})
|
||||
Reference in New Issue
Block a user