const { test } = require('node:test') const assert = require('node:assert/strict') const fs = require('node:fs') const ts = require('typescript') require.extensions['.ts'] = (module, filename) => module._compile(ts.transpileModule(fs.readFileSync(filename, 'utf8'), { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 } }).outputText, filename) const { compactAgentEvidence, missingAgentRead, readFileEvidence } = require('../src/renderer/src/features/workspace/agentEvidence.ts') const { resolveAgentContext } = require('../src/renderer/src/features/workspace/agentContext.ts') const file = (startLine, endLine, extra = {}, result = {}) => ({ id: `read-${startLine}-${endLine}`, name: 'files.read', moduleName: 'Control', target: 'ssh:jetson', status: 'ok', observedAt: '2026-10-20T00:00:00.000Z', truncated: true, data: JSON.stringify({ path: 'config/car.yaml', exists: true, hash: 'a'.repeat(64), content: Array.from({ length: endLine - startLine + 1 }, (_, i) => `line${startLine + i}`).join('\n'), startLine, endLine, totalLines: 100, complete: false, contentTruncated: false, ...extra }), ...result }) const read = (startLine, lineCount, path = 'config/car.yaml') => ({ id: 'read', name: 'files.read', moduleName: 'Control', args: { path, ...(startLine === undefined ? {} : { startLine }), ...(lineCount === undefined ? {} : { lineCount }) } }) const answer = toolRequests => ({ provider: 'test', model: 'test', plan: { summary: 'result', explanation: 'verified code', toolRequests, changes: [], commands: [], actionPlan: [], createModule: null } }) test('overlapping and adjacent ranges merge without promoting partial evidence to editable full files', () => { const results = compactAgentEvidence([file(1, 10), file(8, 15), file(16, 20)]) assert.equal(results.length, 1) const data = readFileEvidence(results[0]) assert.equal(data.startLine, 1) assert.equal(data.endLine, 20) assert.equal(data.content.split('\n').length, 20) assert.equal(data.complete, false) assert.equal(results[0].truncated, true) assert.equal(missingAgentRead(read(3, 16), results), undefined) assert.deepEqual(missingAgentRead(read(), results), read()) }) test('queries only the first missing range and never joins disjoint or conflicting content', () => { const results = compactAgentEvidence([file(1, 5), file(10, 15)]) assert.equal(results.length, 2) assert.deepEqual(missingAgentRead(read(3, 15), results).args, { path: 'config/car.yaml', startLine: 6, lineCount: 4 }) assert.equal(compactAgentEvidence([file(1, 10), file(8, 15, { content: 'different\ncontent' })]).length, 2) }) test('host boundaries and file revisions cannot be combined or used to fill each other’s gaps', () => { const changed = compactAgentEvidence([file(1, 10), file(11, 20, { hash: 'b'.repeat(64) })]) assert.equal(changed.length, 1) assert.equal(missingAgentRead(read(1, 20), changed).args.startLine, 1) const hosts = compactAgentEvidence([file(1, 10, {}, { target: 'local-host' }), file(11, 20)]) assert.equal(hosts.length, 2) assert.equal(missingAgentRead(read(1, 20), hosts).args.startLine, 1) }) test('full and missing observations are reused but partial character fragments are never coverage', () => { const full = file(1, 10, { complete: true }, { truncated: false }) assert.equal(missingAgentRead(read(5, 100), [full]), undefined) assert.equal(compactAgentEvidence([full, file(5, 8)]).length, 1) const missing = file(1, 1, { exists: false, content: '', hash: null, complete: true }, { truncated: false }) assert.equal(missingAgentRead(read(), [missing]), undefined) const cut = file(1, 2, { content: 'whole\ncut', endLine: 1, contentTruncated: true }) assert.equal(missingAgentRead(read(2, 1), [cut]).args.startLine, 2) const legacy = file(1, 2, { endLine: undefined, contentTruncated: undefined }) assert.equal(missingAgentRead(read(2, 1), [legacy]).args.startLine, 2) }) test('malformed or oversized evidence never becomes reusable complete coverage', () => { assert.equal(readFileEvidence(file(1, 10, { hash: 'not-a-hash' })), undefined) assert.equal(readFileEvidence(file(1, 10, { endLine: 999 })), undefined) const large = compactAgentEvidence([file(1, 1, { content: 'a'.repeat(30_000) }), file(2, 2, { content: 'b'.repeat(30_000) })]) assert.equal(large.length, 2) }) test('overlap requests execute only missing lines and finish once the requested range is cached', async () => { const executions = [] let calls = 0 await resolveAgentContext({ initialContext: { modules: [], runtimeLogs: [] }, signal: new AbortController().signal, executeTool: async tool => { executions.push(tool.args); return file(tool.args.startLine, tool.args.startLine + tool.args.lineCount - 1) }, requestPlan: async (context, final) => { calls++ if (final) { assert.equal(context.toolResults.length, 1) assert.equal(JSON.parse(context.toolResults[0].data).endLine, 15) return answer([]) } return answer([[read(1, 10)], [read(5, 11)], [read(4, 10)]][calls - 1]) } }) assert.deepEqual(executions, [read(1, 10).args, read(11, 5).args]) assert.equal(calls, 4) }) test('source evidence survives many later searches and omissions are reported within DTO limits', async () => { const searchResults = Array.from({ length: 15 }, (_, i) => ({ ...file(1, 1), id: `search-${i}`, name: 'files.search', data: JSON.stringify({ query: `speed${i}`, matches: [] }) })) await resolveAgentContext({ initialContext: { modules: [], runtimeLogs: [], toolResults: [file(1, 10), ...searchResults] }, signal: new AbortController().signal, requestPlan: async context => { assert.equal(context.toolResults.length, 12) assert.equal(context.toolResults[0].name, 'files.read') assert.match(context.contextNotes[0], /4개/) assert.ok(context.contextNotes.length <= 12) assert.ok(context.contextNotes.every(note => note.length <= 500)) return answer([]) } }) }) test('a cached file omitted by budgets is restored when the model requests it again', async () => { let calls = 0 const results = Array.from({ length: 13 }, (_, i) => file(1, 10, { path: `file${i}.yaml` })) await resolveAgentContext({ initialContext: { modules: [], runtimeLogs: [], toolResults: results }, signal: new AbortController().signal, executeTool: async () => { throw Error('cached file must not be queried again') }, requestPlan: async (context, final) => { calls++ if (!final) return answer([read(1, 5, 'file0.yaml')]) assert.ok(context.toolResults.some(result => JSON.parse(result.data).path === 'file0.yaml')) return answer([]) } }) assert.equal(calls, 2) }) test('finalization retains verified code locations while stripping all proposed mutations', async () => { const reference = { id: 'code-1', moduleName: 'Control', path: 'config/car.yaml', startLine: 1, endLine: 3, reason: 'verified', expectedHash: 'a'.repeat(64), snippet: 'line1' } const result = await resolveAgentContext({ initialContext: { modules: [], runtimeLogs: [], toolResults: [file(1, 10)] }, signal: new AbortController().signal, executeTool: async () => { throw Error('already cached') }, requestPlan: async (_, final) => ({ ...answer([read(1, 5)]), plan: { ...answer([read(1, 5)]).plan, ...(final ? { changes: [{ path: 'unsafe' }], commands: [{ command: 'unsafe' }] } : { codeReferences: [reference] }) } }) }) assert.equal(result.plan.codeReferences.length, 1) assert.match(result.plan.explanation, /verified code/) assert.deepEqual(result.plan.changes, []) assert.deepEqual(result.plan.commands, []) }) test('recovery never resurrects code locations after a different file revision was observed', async () => { let calls = 0 const reference = { id: 'code-1', moduleName: 'Control', path: 'config/car.yaml', startLine: 1, endLine: 3, reason: 'verified', expectedHash: 'a'.repeat(64), snippet: 'line1' } const result = await resolveAgentContext({ initialContext: { modules: [], runtimeLogs: [], toolResults: [file(1, 10)] }, signal: new AbortController().signal, executeTool: async () => file(11, 20, { hash: 'b'.repeat(64) }), requestPlan: async (_, final) => { calls++ return !final && calls === 1 ? { ...answer([read(11, 10)]), plan: { ...answer([read(11, 10)]).plan, codeReferences: [reference] } } : answer([read(11, 10)]) } }) assert.deepEqual(result.plan.codeReferences, []) assert.match(result.plan.summary, /확정하지 못했습니다/) })