106 lines
5.9 KiB
JavaScript
106 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 api = {}
|
|
vm.runInNewContext(ts.transpileModule(fs.readFileSync(require.resolve('../src/renderer/src/features/workspace/agentConversationContext.ts'), 'utf8'), {
|
|
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 }
|
|
}).outputText, { exports: api })
|
|
const { agentConversationWorkspace: scope, buildAgentConversationContext: build } = api
|
|
const now = Date.now()
|
|
const local = scope('local', 'local-host', '/workspace/a')
|
|
const plain = value => JSON.parse(JSON.stringify(value))
|
|
const turn = (n, workspace = local, createdAt = now) => [
|
|
{ id: `u${n}`, role: 'user', content: `question-${n}`, conversationScope: { workspace, createdAt } },
|
|
{ id: `a${n}`, role: 'assistant', content: `summary-${n}`, conversationScope: { workspace, createdAt }, plan: {
|
|
summary: `summary-${n}`, explanation: `answer-${n}`, conversationSummary: `누적 기억-${n}`, changes: [], commands: [], requestedModules: [], requestedLogs: [], createModule: null
|
|
} }
|
|
]
|
|
|
|
test('uses the latest cumulative summary with only two recent exchanges and no duplicate current question', () => {
|
|
const messages = Array.from({ length: 20 }, (_, i) => turn(i)).flat()
|
|
messages.push({ ...turn(20)[0], content: 'pending current question' })
|
|
const context = build(messages, local, now)
|
|
assert.deepEqual(plain(context.recentMessages).map(item => item.content), ['question-18', 'summary-18\nanswer-18', 'question-19', 'summary-19\nanswer-19'])
|
|
assert.equal(context.summary, '누적 기억-19')
|
|
assert.doesNotMatch(JSON.stringify(context), /question-17|pending current question/)
|
|
})
|
|
|
|
test('hard caps payload text even with huge original answers and requests', () => {
|
|
const messages = Array.from({ length: 10 }, (_, i) => {
|
|
const pair = turn(i)
|
|
pair[0].content = 'q'.repeat(4000)
|
|
pair[1].plan.summary = 's'.repeat(500)
|
|
pair[1].plan.explanation = 'e'.repeat(12000)
|
|
pair[1].plan.conversationSummary = 'm'.repeat(2000)
|
|
return pair
|
|
}).flat()
|
|
const context = build(messages, local, now)
|
|
assert.ok(context.summary.length <= 1000)
|
|
assert.equal(context.recentMessages.length, 4)
|
|
assert.ok(context.recentMessages.every(message => message.content.length <= 700))
|
|
assert.ok(context.summary.length + context.recentMessages.reduce((sum, item) => sum + item.content.length, 0) <= 3800)
|
|
})
|
|
|
|
test('does not mix hosts, roots, SSH identities, unscoped legacy messages or expired history', () => {
|
|
for (const other of [scope('remote', 'ssh:a', '/workspace/a'), scope('local', 'local-host', '/workspace/b'), scope('remote', 'ssh:b', '/workspace/a')]) {
|
|
assert.equal(build(turn(1, other), local, now), undefined)
|
|
}
|
|
assert.equal(build(turn(1, local, now - 86400000), local, now), undefined)
|
|
assert.equal(build(turn(1, local, now + 1), local, now), undefined)
|
|
assert.equal(build(turn(1).map(({ conversationScope, ...message }) => message), local, now), undefined)
|
|
assert.equal(scope('local', 'local-host'), undefined)
|
|
assert.equal(build(turn(1), undefined, now), undefined)
|
|
assert.equal(build([], local, now), undefined)
|
|
})
|
|
|
|
test('a missing or empty generated summary preserves earlier valid memory without an extra model call', () => {
|
|
const first = turn(1), second = turn(2)
|
|
delete second[1].plan.conversationSummary
|
|
assert.equal(build([...first, ...second], local, now).summary, '누적 기억-1')
|
|
second[1].plan.conversationSummary = ''
|
|
assert.equal(build([...first, ...second], local, now).summary, '누적 기억-1')
|
|
delete first[1].plan.conversationSummary
|
|
assert.equal(build([...first, ...second], local, now).summary, '')
|
|
second[1].plan.conversationSummary = '새 누적 요약'
|
|
assert.equal(build([...first, ...second], local, now).summary, '새 누적 요약')
|
|
})
|
|
|
|
test('keeps file identities and action outcomes, but excludes payloads and tool evidence', () => {
|
|
const pair = turn(1)
|
|
pair[1].plan.explanation = '설명\n```python\nPRIVATE_SOURCE\n```\n끝'
|
|
pair[1].plan.changes = [{ moduleName: 'Control', path: 'config.yaml', content: 'PRIVATE_CHANGE' }]
|
|
pair[1].plan.commands = [{ id: 'build', moduleName: 'Control', purpose: 'build', command: 'PRIVATE_COMMAND' }]
|
|
pair[1].plan.evidence = [{ data: 'PRIVATE_LOG' }]
|
|
pair[1].plan.codeReferences = [{ moduleName: 'Control', path: 'node.py', startLine: 20, endLine: 23, snippet: 'PRIVATE_SNIPPET' }]
|
|
pair[1].commandExecutions = { build: { status: 'completed', output: 'PRIVATE_OUTPUT' } }
|
|
pair[1].applyState = 'cancelled'
|
|
const context = JSON.stringify(build(pair, local, now))
|
|
assert.match(context, /Control\/node.py:20-23/)
|
|
assert.match(context, /file change: cancelled/)
|
|
assert.match(context, /build: completed/)
|
|
assert.doesNotMatch(context, /PRIVATE_/)
|
|
})
|
|
|
|
test('ignores errors, interrupted turns and initial lookup responses', () => {
|
|
const pair = turn(1)
|
|
const error = { ...pair[1], plan: undefined, content: 'network error' }
|
|
assert.equal(build([pair[0], error], local, now), undefined)
|
|
pair[1].plan.toolRequests = [{ name: 'files.read' }]
|
|
assert.equal(build(pair, local, now), undefined)
|
|
assert.equal(build([turn(1)[0], ...turn(2, scope('remote', 'ssh:x', '/workspace'))], local, now), undefined)
|
|
})
|
|
|
|
test('rebuilds from persisted metadata and reflects updated execution outcomes without new memory storage', () => {
|
|
const pair = turn(1)
|
|
pair[1].plan.commands = [{ id: 'build', moduleName: 'Control', purpose: 'build' }]
|
|
assert.match(JSON.stringify(build(plain(pair), local, now)), /pending/)
|
|
pair[1].commandExecutions = { build: { status: 'failed' } }
|
|
assert.match(JSON.stringify(build(plain(pair), local, now)), /failed/)
|
|
const followUp = { ...pair[1], content: 'result', plan: { ...pair[1].plan, summary: '실행 결과 분석', explanation: '실패했습니다.' } }
|
|
assert.match(JSON.stringify(build([...pair, followUp], local, now)), /실행 결과 분석/)
|
|
assert.equal(build([], local, now), undefined)
|
|
})
|