fix: show agent messages before workspace lookup and bump to 1.2.24

This commit is contained in:
2026-10-20 12:57:12 +09:00
parent 065de83ffd
commit af6dc560ea
7 changed files with 123 additions and 15 deletions
+86
View File
@@ -47,6 +47,92 @@ function harness(requestApi) {
return { ...api, state, selectedModel, messageStateRef, advance: ms => { now += ms } }
}
test('shows and persists the question, pending state and timer before a slow module snapshot finishes', async () => {
const snapshot = deferred(), completion = deferred()
const bodies = []
let loads = 0
const h = harness((_, request) => { bodies.push(JSON.parse(request.body)); return completion.promise })
const running = h.requestPlan('원격 차량 속도 제한 위치', [], [], {
environment: 'remote', workspaceTarget: 'ssh:100.64.0.10', executeTool: async () => result(),
loadInitialContext: signal => { loads++; assert.equal(signal.aborted, false); return snapshot.promise }
})
assert.equal(h.state.messages[0].content, '원격 차량 속도 제한 위치')
assert.equal(h.state.saved[0][0].role, 'user')
assert.equal(h.state.pending, true)
assert.equal(h.state.requestStartedAt, 1000)
assert.equal(h.state.activity, '모듈 상태 조회 중')
assert.equal(bodies.length, 0)
await h.requestPlan('중복 전송', [], [], { loadInitialContext: () => { loads++; return snapshot.promise } })
assert.equal(loads, 1)
assert.equal(h.state.messages.length, 1)
h.advance(5000)
h.selectedModel.provider = 'OPENAI'
snapshot.resolve({ modules: [{ name: 'Control', state: 'running', parameters: ['sim_mode:=false'], files: [] }], runtimeLogs: [] })
await tick()
assert.equal(bodies.length, 1)
assert.equal(bodies[0].provider, 'QWEN')
assert.equal(bodies[0].modules[0].state, 'running')
assert.deepEqual(bodies[0].modules[0].parameters, ['sim_mode:=false'])
h.advance(2000)
completion.resolve(response())
await running
assert.equal(h.state.messages[1].durationMs, 7000)
assert.equal(h.state.pending, false)
})
for (const reject of [false, true]) test(`cancels during initial module lookup and ignores its late ${reject ? 'failure' : 'result'} without affecting a new request`, async () => {
const old = deferred(), next = deferred()
let calls = 0, signal
const h = harness(async () => { calls++; return response() })
const first = h.requestPlan('첫 질문', [], [], { loadInitialContext: current => { signal = current; return old.promise } })
h.stop()
assert.equal(signal.aborted, true)
assert.equal(h.state.pending, false)
const second = h.requestPlan('다음 질문', [], [], { loadInitialContext: () => next.promise })
if (reject) old.reject(Error('늦게 도착한 SSH 오류'))
else old.resolve({ modules: [], runtimeLogs: [] })
await first
assert.equal(calls, 0)
assert.equal(h.state.pending, true)
assert.equal(h.state.activity, '모듈 상태 조회 중')
assert.equal(h.state.messages.length, 2)
assert.equal(h.state.notices, 0)
next.resolve({ modules: [], runtimeLogs: [] })
await second
assert.equal(calls, 1)
assert.equal(h.state.messages.length, 3)
})
test('initial snapshot errors preserve the question and show a timed error without calling the model', async () => {
const snapshot = deferred()
let calls = 0
const h = harness(async () => { calls++; return response() })
const running = h.requestPlan('질문', [], [], { loadInitialContext: () => snapshot.promise })
h.advance(3000)
snapshot.reject(Error('원격 모듈 상태 조회 실패'))
await running
assert.equal(calls, 0)
assert.equal(h.state.messages[0].content, '질문')
assert.equal(h.state.messages[1].content, '원격 모듈 상태 조회 실패')
assert.equal(h.state.messages[1].durationMs, 3000)
assert.equal(h.state.pending, false)
assert.equal(h.state.requestStartedAt, null)
assert.equal(h.state.activity, '')
})
test('scope changes during initial snapshot loading suppress later API calls and errors', async () => {
const snapshot = deferred()
let calls = 0
const h = harness(async () => { calls++; return response() })
const running = h.requestPlan('이전 강좌 질문', [], [], { loadInitialContext: () => snapshot.promise })
h.messageStateRef.current.session = {}
snapshot.resolve({ modules: [], runtimeLogs: [] })
await running
assert.equal(calls, 0)
assert.equal(h.state.messages.length, 1)
assert.equal(h.state.notices, 0)
})
test('publishes completed stages immediately without persisting progress or adding model calls', async () => {
const first = deferred(), tool = deferred(), final = deferred()
let calls = 0
+15 -6
View File
@@ -15,6 +15,8 @@ const plain = value => JSON.parse(JSON.stringify(value))
function harness(remoteWorkspace) {
const executions = []
const calls = []
const contexts = []
const events = []
const errors = []
const target = { host: '100.64.0.10', user: 'fixture', workspaceRoot: '/fixture/workspace' }
const live = [{ name: 'NAV2-MKMini', folderName: 'vehicle', state: 'running', parameters: ['sim_mode:=false', 'robot:=mkmini'] }]
@@ -26,10 +28,14 @@ function harness(remoteWorkspace) {
getRemoteWorkspaceTarget: () => target, applyModuleParameterOverrides: modules => modules,
validateAgentTool: request => request, AGENT_PRACTICE_PROFILES: [],
window: {
remoteWorkspace: { modules: { list: async () => ({ modules: live }) }, agent: { readTool: readTool('remote') } },
localWorkspace: { modules: { list: async () => ({ modules: live }) }, agent: { readTool: readTool('local') } }
remoteWorkspace: { modules: { list: async () => { events.push('snapshot-read'); return { modules: live } } }, agent: { readTool: readTool('remote') } },
localWorkspace: { modules: { list: async () => { events.push('snapshot-read'); return { modules: live } } }, agent: { readTool: readTool('local') } }
},
requestAgentPlan: async (...args) => {
calls.push(args)
events.push('plan-start')
contexts.push(await args[3].loadInitialContext(new AbortController().signal))
},
requestAgentPlan: async (...args) => { calls.push(args) },
onTitlebarNotice: (...args) => { errors.push(args) }, console: { error: () => {} },
loadAgentRequestedContext: async () => ({ modules: [], runtimeLogs: [] })
}
@@ -37,16 +43,19 @@ function harness(remoteWorkspace) {
vm.runInNewContext(ts.transpileModule(names.map(name => `const ${name} = ${declarations.get(name)}; exports.${name} = ${name}`).join('\n'), {
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 }
}).outputText, globals)
return { ...exports, executions, calls, errors }
return { ...exports, executions, calls, contexts, events, errors }
}
for (const remote of [false, true]) {
test(`${remote ? 'remote' : 'local'}: a new question refreshes state and start settings before sending context`, async () => {
test(`${remote ? 'remote' : 'local'}: enters the chat lifecycle immediately and refreshes state before the provider request`, async () => {
const h = harness(remote)
await h.submitAgentRequest('최대 속도 제한 코드 위치')
assert.deepEqual(h.errors, [])
assert.equal(h.calls.length, 1)
const [, modules, , options] = h.calls[0]
const [, initialModules, , options] = h.calls[0]
assert.deepEqual(plain(initialModules), [])
assert.deepEqual(h.events, ['plan-start', 'snapshot-read'])
const { modules } = h.contexts[0]
assert.equal(modules[0].state, 'running')
assert.deepEqual(plain(modules[0].parameters), ['sim_mode:=false', 'robot:=mkmini'])
assert.deepEqual(plain(modules[0].files), [])