97 lines
4.7 KiB
JavaScript
97 lines
4.7 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 contextApi = {}
|
|
const evidenceApi = {}
|
|
vm.runInNewContext(ts.transpileModule(fs.readFileSync(require.resolve('../src/renderer/src/features/workspace/agentEvidence.ts'), 'utf8'), {
|
|
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 }
|
|
}).outputText, { exports: evidenceApi })
|
|
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: contextApi, DOMException, require: name => { assert.equal(name, './agentEvidence'); return evidenceApi } })
|
|
|
|
const source = ts.createSourceFile('useAiAgent.ts', fs.readFileSync(require.resolve('../src/renderer/src/features/workspace/useAiAgent.ts'), 'utf8'), ts.ScriptTarget.Latest, true)
|
|
let payloadExpression
|
|
function visit(node) {
|
|
if (ts.isCallExpression(node) && node.expression.getText(source) === 'JSON.stringify' && node.arguments[0]?.getText(source).includes('imageTokens')) {
|
|
payloadExpression = node.getText(source)
|
|
}
|
|
ts.forEachChild(node, visit)
|
|
}
|
|
visit(source)
|
|
assert.ok(payloadExpression, 'Agent request JSON serialization must exist')
|
|
|
|
test('user messages preserve only typed text while attachments remain separate metadata', () => {
|
|
let initializer
|
|
function find(node) {
|
|
if (ts.isVariableDeclaration(node) && node.name.getText(source) === 'userMessage') initializer = node.initializer.getText(source)
|
|
ts.forEachChild(node, find)
|
|
}
|
|
find(source)
|
|
assert.ok(initializer)
|
|
for (const request of ['이 이미지에서 슬램에 필요한 모듈만 골라봐', '첫 줄\n첨부 파일: 직접 입력한 내용']) {
|
|
const message = vm.runInNewContext(`(${initializer})`, { request, options: { attachments: [{ name: '스크린샷.png' }] } })
|
|
assert.equal(message.content, request)
|
|
}
|
|
})
|
|
|
|
function serialize(attachments, finalize = false, request = '설정 위치 알려줘', imageTokens = [], provider = 'QWEN') {
|
|
return JSON.parse(vm.runInNewContext(payloadExpression, {
|
|
courseId: 'course-1', weekNumber: 3, request, options: { attachments }, finalize,
|
|
textAttachments: attachments ?? [], imageTokens,
|
|
selection: provider ? { provider } : {},
|
|
agentPlanRequestText: contextApi.agentPlanRequestText,
|
|
context: { modules: [{ name: 'Control', files: [] }], runtimeLogs: [] }
|
|
}))
|
|
}
|
|
|
|
test('sends image reference tokens without image bytes or public URLs on every context round', () => {
|
|
const body = serialize(undefined, true, '이미지 분석', ['signed-image-token'])
|
|
assert.deepEqual(body.imageTokens, ['signed-image-token'])
|
|
assert.equal(Object.hasOwn(body, 'attachments'), false)
|
|
assert.equal(JSON.stringify(body).includes('base64'), false)
|
|
})
|
|
|
|
for (const attachments of [undefined, []]) {
|
|
test(`omits attachments when ${attachments ? 'empty' : 'undefined'}`, () => {
|
|
const body = serialize(attachments)
|
|
assert.equal(Object.hasOwn(body, 'attachments'), false)
|
|
assert.equal(body.request, '설정 위치 알려줘')
|
|
assert.equal(body.modules[0].name, 'Control')
|
|
assert.equal(body.week, 3)
|
|
})
|
|
}
|
|
|
|
test('preserves actual attachment contents rather than silently dropping them', () => {
|
|
const attachments = [{ name: 'config.yaml', content: 'steering: ackermann' }]
|
|
assert.deepEqual(serialize(attachments).attachments, attachments)
|
|
})
|
|
|
|
test('finalization instruction reaches the API without sending unsupported DTO fields', () => {
|
|
const body = serialize(undefined, true)
|
|
assert.match(body.request, /자료 조회는 종료/)
|
|
assert.match(body.request, /설정 위치 알려줘/)
|
|
assert.match(body.request, /최종 답변/)
|
|
assert.equal(Object.hasOwn(body, 'finalize'), false)
|
|
assert.deepEqual(Object.keys(body).sort(), ['courseId', 'modules', 'provider', 'request', 'runtimeLogs', 'week'])
|
|
})
|
|
|
|
test('finalization keeps the request within the backend 4000-character limit', () => {
|
|
const body = serialize(undefined, true, 'x'.repeat(4000))
|
|
assert.equal(body.request.length, 4000)
|
|
assert.match(body.request, /원래 학생 질문/)
|
|
})
|
|
|
|
test('sends only a provider selection rather than student-supplied models or keys', () => {
|
|
for (const finalize of [false, true]) {
|
|
const body = serialize(undefined, finalize, '확인', [], 'OPENAI')
|
|
assert.equal(body.provider, 'OPENAI')
|
|
assert.equal(Object.hasOwn(body, 'model'), false)
|
|
assert.equal(Object.hasOwn(body, 'apiKey'), false)
|
|
assert.equal(Object.hasOwn(body, 'endpointUrl'), false)
|
|
}
|
|
assert.equal(Object.hasOwn(serialize(undefined, false, '확인', [], null), 'provider'), false)
|
|
})
|