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: [] }) })) })