feat(chat): add agent attachments and shared conversation confirmation
This commit is contained in:
@@ -0,0 +1,135 @@
|
||||
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 output = {}
|
||||
vm.runInNewContext(ts.transpileModule(fs.readFileSync(require.resolve('../src/renderer/src/features/workspace/agentContext.ts'), 'utf8'), {
|
||||
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 }
|
||||
}).outputText, { exports: output, DOMException })
|
||||
const { resolveAgentContext } = output
|
||||
const plain = value => JSON.parse(JSON.stringify(value))
|
||||
const moduleInfo = (name, files = []) => ({ name, state: 'stopped', files })
|
||||
const response = (requestedModules = [], requestedLogs = []) => ({
|
||||
provider: 'QWEN', model: 'test', usage: { totalTokens: 10 },
|
||||
plan: { summary: 'result', explanation: 'analysis', requestedModules, requestedLogs, actionPlan: [], commands: [], changes: [], createModule: null }
|
||||
})
|
||||
const options = overrides => ({
|
||||
initialContext: { modules: [moduleInfo('MKMini'), moduleInfo('Control')], runtimeLogs: [] },
|
||||
signal: new AbortController().signal,
|
||||
...overrides
|
||||
})
|
||||
|
||||
test('continues MKMini -> Control -> final analysis and retains earlier source files', async () => {
|
||||
const inputs = []
|
||||
const loads = []
|
||||
const result = await resolveAgentContext(options({
|
||||
requestPlan: async context => {
|
||||
inputs.push(plain(context))
|
||||
return [response(['MKMini']), response(['Control']), response()][inputs.length - 1]
|
||||
},
|
||||
loadRequestedContext: async request => {
|
||||
loads.push(plain(request))
|
||||
return { modules: ['MKMini', 'Control'].map(name => moduleInfo(name, request.modules.includes(name) ? [{ path: `${name}.py`, content: `${name} source` }] : [])), runtimeLogs: [] }
|
||||
}
|
||||
}))
|
||||
assert.equal(inputs.length, 3)
|
||||
assert.deepEqual(loads, [{ modules: ['MKMini'], logs: [] }, { modules: ['Control'], logs: [] }])
|
||||
assert.equal(inputs[2].modules[0].files[0].content, 'MKMini source')
|
||||
assert.equal(inputs[2].modules[1].files[0].content, 'Control source')
|
||||
assert.deepEqual(plain(result.plan.requestedModules), [])
|
||||
assert.equal(result.usage.totalTokens, 30)
|
||||
})
|
||||
|
||||
test('keeps logs while loading a later source module', async () => {
|
||||
let calls = 0
|
||||
await resolveAgentContext(options({
|
||||
requestPlan: async context => {
|
||||
calls += 1
|
||||
if (calls === 3) assert.equal(context.runtimeLogs[0].content, 'error log')
|
||||
return [response([], ['Control']), response(['Control']), response()][calls - 1]
|
||||
},
|
||||
loadRequestedContext: async request => ({
|
||||
modules: [moduleInfo('Control', request.modules.length ? [{ path: 'node.py', content: 'code' }] : [])],
|
||||
runtimeLogs: request.logs.length ? [{ moduleName: 'Control', state: 'running', content: 'error log' }] : []
|
||||
})
|
||||
}))
|
||||
assert.equal(calls, 3)
|
||||
})
|
||||
|
||||
test('rejects repeated requests instead of returning a stalled plan', async () => {
|
||||
let loads = 0
|
||||
await assert.rejects(resolveAgentContext(options({
|
||||
requestPlan: async () => response(['Control']),
|
||||
loadRequestedContext: async () => { loads += 1; return { modules: [moduleInfo('Control')], runtimeLogs: [] } }
|
||||
})), /반복 요청/)
|
||||
assert.equal(loads, 1)
|
||||
})
|
||||
|
||||
test('requests only new resources when the model also repeats an earlier module', async () => {
|
||||
let count = 0
|
||||
const loads = []
|
||||
await resolveAgentContext(options({
|
||||
requestPlan: async () => [response(['MKMini']), response(['MKMini', 'Control']), response()][count++],
|
||||
loadRequestedContext: async request => { loads.push(plain(request)); return { modules: request.modules.map(name => moduleInfo(name)), runtimeLogs: [] } }
|
||||
}))
|
||||
assert.deepEqual(loads[1], { modules: ['Control'], logs: [] })
|
||||
})
|
||||
|
||||
test('does not start another API call after cancellation during context loading', async () => {
|
||||
const controller = new AbortController()
|
||||
let calls = 0
|
||||
await assert.rejects(resolveAgentContext(options({
|
||||
signal: controller.signal,
|
||||
requestPlan: async () => { calls += 1; return response(['Control']) },
|
||||
loadRequestedContext: async () => { controller.abort(); return { modules: [], runtimeLogs: [] } }
|
||||
})), { name: 'AbortError' })
|
||||
assert.equal(calls, 1)
|
||||
})
|
||||
|
||||
test('bounds the number of distinct automatic context requests', async () => {
|
||||
let calls = 0
|
||||
await assert.rejects(resolveAgentContext(options({
|
||||
requestPlan: async () => response([`Module${calls++}`]),
|
||||
loadRequestedContext: async request => ({ modules: request.modules.map(name => moduleInfo(name)), runtimeLogs: [] })
|
||||
})), /5회/)
|
||||
assert.equal(calls, 6)
|
||||
})
|
||||
|
||||
test('passes file-read failures to the model for a concrete final explanation', async () => {
|
||||
let count = 0
|
||||
await resolveAgentContext(options({
|
||||
requestPlan: async context => {
|
||||
if (count++ === 0) return response(['Control'])
|
||||
assert.equal(context.modules.find(module => module.name === 'Control').fileContext.status, 'error')
|
||||
return response()
|
||||
},
|
||||
loadRequestedContext: async () => ({ modules: [{ ...moduleInfo('Control'), fileContext: { status: 'error', message: 'permission denied' } }], runtimeLogs: [] })
|
||||
}))
|
||||
})
|
||||
|
||||
test('does not silently truncate accumulated source context beyond API limits', async () => {
|
||||
await assert.rejects(resolveAgentContext(options({
|
||||
requestPlan: async () => response(['Control']),
|
||||
loadRequestedContext: async () => ({ modules: [moduleInfo('Control', [{ path: 'large.py', content: 'x'.repeat(160_001) }])], runtimeLogs: [] })
|
||||
})), /조회 한도/)
|
||||
})
|
||||
|
||||
test('does not display a context-only response when no loader is available', async () => {
|
||||
await assert.rejects(resolveAgentContext(options({ requestPlan: async () => response(['Control']) })), /읽을 수 없는/)
|
||||
})
|
||||
|
||||
test('keeps a just-started module running when a stale collector supplies source files', async () => {
|
||||
let calls = 0
|
||||
await resolveAgentContext(options({
|
||||
initialContext: { modules: [{ ...moduleInfo('Control'), state: 'running' }], runtimeLogs: [] },
|
||||
requestPlan: async context => {
|
||||
if (calls++ === 0) return response(['Control'])
|
||||
assert.equal(context.modules[0].state, 'running')
|
||||
assert.equal(context.modules[0].files[0].content, 'code')
|
||||
return response()
|
||||
},
|
||||
loadRequestedContext: async () => ({ modules: [moduleInfo('Control', [{ path: 'node.py', content: 'code' }])], runtimeLogs: [] })
|
||||
}))
|
||||
})
|
||||
Reference in New Issue
Block a user