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 = {} 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: output, DOMException, require: name => { assert.equal(name, './agentEvidence'); return evidenceApi } }) 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('makes one analysis-only completion when the model repeats a context request', async () => { let loads = 0 let calls = 0 const result = await resolveAgentContext(options({ requestPlan: async (context, finalize) => { calls++ if (!finalize) return response(['Control']) assert.equal(context.modules.find(module => module.name === 'Control').files[0].path, 'controller.py') return { ...response(), plan: { ...response().plan, explanation: 'controller.py에서 조향 설정을 확인했습니다.' } } }, loadRequestedContext: async () => { loads += 1; return { modules: [moduleInfo('Control', [{ path: 'controller.py', content: 'steering = 1' }])], runtimeLogs: [] } } })) assert.equal(loads, 1) assert.equal(calls, 3) assert.match(result.plan.explanation, /조향 설정을 확인/) assert.equal(result.usage.totalTokens, 30) }) test('finishes honestly without an infinite loop if the model ignores the finalization request', async () => { let loads = 0 let calls = 0 const result = await resolveAgentContext(options({ requestPlan: async () => { calls++; return response(['Control']) }, loadRequestedContext: async () => { loads += 1; return { modules: [moduleInfo('Control')], runtimeLogs: [] } } })) assert.equal(loads, 1) assert.equal(calls, 3) assert.deepEqual(plain(result.plan.requestedModules), []) assert.deepEqual(plain(result.plan.actionPlan), []) assert.match(result.plan.explanation, /결론을 확정하지 못했습니다/) assert.match(result.plan.explanation, /Control: 제공된 파일 0개/) assert.equal(result.plan.createModule, null) }) test('uses already provided source files without loading the same module again', async () => { let loads = 0 let calls = 0 const result = await resolveAgentContext(options({ initialContext: { modules: [moduleInfo('Control', [{ path: 'control.py', content: 'steering = 1' }])], runtimeLogs: [] }, requestPlan: async (_, finalize) => { calls++; return finalize ? response() : response(['Control']) }, loadRequestedContext: async () => { loads++; return { modules: [], runtimeLogs: [] } } })) assert.equal(loads, 0) assert.equal(calls, 2) assert.deepEqual(plain(result.plan.requestedModules), []) }) test('recovery cannot turn repeated source requests into executable commands or edits', async () => { const result = await resolveAgentContext(options({ initialContext: { modules: [moduleInfo('Control', [{ path: 'control.py', content: 'source' }])], runtimeLogs: [] }, requestPlan: async (_, finalize) => finalize ? { ...response(), plan: { ...response().plan, actionPlan: ['execute'], commands: [{ id: 'run', command: './run.sh' }], changes: [{ moduleName: 'Control', path: 'control.py', content: 'changed' }], createModule: { name: 'new' } } } : response(['Control']), loadRequestedContext: async () => ({ modules: [], runtimeLogs: [] }) })) for (const field of ['commands', 'changes', 'actionPlan']) assert.deepEqual(plain(result.plan[field]), []) assert.equal(result.plan.createModule, null) }) test('does not publish a finalization response received after cancellation', async () => { const controller = new AbortController() let calls = 0 await assert.rejects(resolveAgentContext(options({ signal: controller.signal, requestPlan: async (_, finalize) => { calls++ if (finalize) controller.abort() return finalize ? response() : response(['Control']) } })), { name: 'AbortError' }) assert.equal(calls, 2) }) 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 let loads = 0 const result = await resolveAgentContext(options({ requestPlan: async (_, finalize) => { calls++; return finalize ? response() : response([`Module${calls}`]) }, loadRequestedContext: async request => { loads++; return { modules: request.modules.map(name => moduleInfo(name)), runtimeLogs: [] } } })) assert.equal(calls, 7) assert.equal(loads, 5) assert.deepEqual(plain(result.plan.requestedModules), []) }) 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('excludes oversized source files with an explicit notice and continues with complete smaller files', async () => { let calls = 0 const smallFile = { path: 'config/steering.yaml', content: 'kinematics: ackermann' } await resolveAgentContext(options({ requestPlan: async context => { if (calls++ === 0) return response(['Control']) const control = context.modules.find(module => module.name === 'Control') assert.deepEqual(plain(control.files), [smallFile]) assert.equal(control.fileContext.status, 'limited') assert.match(control.fileContext.message, /전체 2개 중 1개/) return response() }, loadRequestedContext: async () => ({ modules: [moduleInfo('Control', [{ path: 'large.py', content: 'x'.repeat(160_001) }, smallFile])], runtimeLogs: [] }) })) assert.equal(calls, 2) }) test('requests a final answer rather than leaving a context-only response when no loader is available', async () => { let calls = 0 const result = await resolveAgentContext(options({ requestPlan: async (_, finalize) => { calls++; return finalize ? response() : response(['Control']) } })) assert.equal(calls, 2) assert.deepEqual(plain(result.plan.requestedModules), []) }) 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: [] }) })) }) const files = (name, count, size = 10) => Array.from({ length: count }, (_, index) => ({ path: `${name}-${index}.py`, content: String(index).padEnd(size, 'x') })) const sourceSize = modules => modules.reduce((sum, module) => sum + module.files.reduce((total, file) => total + file.path.length + file.content.length, 0), 0) test('balances 24 old and 24 new files instead of failing the second module lookup', async () => { let calls = 0 await resolveAgentContext(options({ requestPlan: async context => { calls++ if (calls === 1) return response(['MKMini']) if (calls === 2) return response(['Control']) assert.equal(context.modules.reduce((sum, module) => sum + module.files.length, 0), 24) for (const name of ['MKMini', 'Control']) { const module = context.modules.find(module => module.name === name) assert.equal(module.files.length, 12) assert.equal(module.fileContext.status, 'limited') assert.match(module.fileContext.message, /전체 24개 중 12개/) } return response() }, loadRequestedContext: async request => ({ modules: request.modules.map(name => moduleInfo(name, files(name, 24))), runtimeLogs: [] }) })) assert.equal(calls, 3) }) test('enforces the combined character budget without slicing source file contents', async () => { let calls = 0 const contents = new Set(['a'.repeat(60_000), 'b'.repeat(60_000)]) await resolveAgentContext(options({ requestPlan: async context => { if (calls++ === 0) return response(['Control']) assert.ok(sourceSize(context.modules) <= 160_000) for (const module of context.modules) { assert.equal(module.files.length, 1) assert.ok(contents.has(module.files[0].content)) assert.equal(module.fileContext.status, 'limited') } return response() }, loadRequestedContext: async () => ({ modules: ['MKMini', 'Control'].map((name, index) => moduleInfo(name, [ { path: 'one.py', content: [...contents][index] }, { path: 'two.py', content: [...contents][index] } ])), runtimeLogs: [] }), initialContext: { modules: [moduleInfo('MKMini', [{ path: 'one.py', content: [...contents][0] }, { path: 'two.py', content: [...contents][0] }]), moduleInfo('Control')], runtimeLogs: [] } })) }) test('prioritizes newly requested source when earlier context nearly fills the character budget', async () => { let calls = 0 await resolveAgentContext(options({ initialContext: { modules: [moduleInfo('MKMini', files('old', 2, 70_000)), moduleInfo('Control')], runtimeLogs: [] }, requestPlan: async context => { if (calls++ === 0) return response(['Control']) assert.equal(context.modules.find(module => module.name === 'Control').files[0].content, 'new'.repeat(20_000)) assert.ok(context.modules.find(module => module.name === 'MKMini').files.length > 0) assert.ok(sourceSize(context.modules) <= 160_000) return response() }, loadRequestedContext: async () => ({ modules: [moduleInfo('Control', [{ path: 'controller.py', content: 'new'.repeat(20_000) }])], runtimeLogs: [] }) })) }) test('budgets initial source context before sending the first API request', async () => { const initialContext = { modules: [moduleInfo('Control', files('source', 30))], runtimeLogs: [] } await resolveAgentContext(options({ initialContext, requestPlan: async context => { assert.equal(context.modules[0].files.length, 24) assert.equal(context.modules[0].fileContext.status, 'limited') return response() } })) assert.equal(initialContext.modules[0].files.length, 30) assert.equal(initialContext.modules[0].fileContext, undefined) }) test('rotates older logs out with a notice rather than failing on a fourth module', async () => { let calls = 0 await resolveAgentContext(options({ initialContext: { modules: [], runtimeLogs: ['A', 'B', 'C'].map(moduleName => ({ moduleName, state: 'running', content: `${moduleName} log` })) }, requestPlan: async context => { if (calls++ === 0) return response([], ['D']) assert.deepEqual(plain(context.runtimeLogs.map(log => log.moduleName)), ['D', 'C', 'B']) assert.match(context.runtimeLogs[0].message, /이전 로그 제외: A/) return response() }, loadRequestedContext: async () => ({ modules: [], runtimeLogs: [{ moduleName: 'D', state: 'running', content: 'latest error' }] }) })) }) test('counts log messages in the server budget and keeps recent log content', async () => { await resolveAgentContext(options({ initialContext: { modules: [], runtimeLogs: ['A', 'B', 'C'].map(moduleName => ({ moduleName, state: 'running', content: `${'x'.repeat(20_000)}LATEST`, message: 'm'.repeat(500) })) }, requestPlan: async context => { assert.equal(context.runtimeLogs.length, 3) assert.ok(context.runtimeLogs.reduce((sum, log) => sum + log.content.length + (log.message?.length ?? 0), 0) <= 60_000) for (const log of context.runtimeLogs) { assert.ok(log.content.endsWith('LATEST')) assert.match(log.message, /최근 구간/) assert.ok(log.message.length <= 500) } return response() } })) }) test('collects candidates from every requested module and prioritizes configuration before large source trees', async () => { const source = ts.createSourceFile('App.tsx', fs.readFileSync(require.resolve('../src/renderer/src/App.tsx'), 'utf8'), ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX) let collector function visit(node) { if (ts.isVariableDeclaration(node) && node.name.getText(source) === 'collectAgentWorkspace') collector = node.initializer.getText(source) ts.forEachChild(node, visit) } visit(source) assert.ok(collector) const api = {} vm.runInNewContext(ts.transpileModule(`exports.collect = ${collector}`, { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 } }).outputText, { exports: api, availableModules: ['MKMini', 'Control'].map(name => ({ name, state: 'stopped' })), remoteWorkspace: false, clonePathPreview: '/workspace', currentActiveEditorFile: null, editorFiles: {}, window: { localWorkspace: { moduleFiles: { read: async name => ({ files: Object.fromEntries([ ...files(name, 30).map((file, index) => [`src/node${index}.py`, file.content]), ['config/steering.yaml', 'kinematics: ackermann'] ]) }) } } } }) const modules = await api.collect(['MKMini', 'Control']) assert.deepEqual(plain(modules.map(module => module.files.length)), [24, 24]) for (const module of modules) assert.equal(module.files[0].path, 'config/steering.yaml') let calls = 0 await resolveAgentContext(options({ requestPlan: async context => { if (calls++ === 0) return response(['MKMini', 'Control']) for (const module of context.modules) { assert.equal(module.files.length, 12) assert.equal(module.files[0].path, 'config/steering.yaml') } return response() }, loadRequestedContext: async request => ({ modules: await api.collect(request.modules), runtimeLogs: [] }) })) })