diff --git a/app/test/llm.test.js b/app/test/llm.test.js index b99dd94..169c872 100644 --- a/app/test/llm.test.js +++ b/app/test/llm.test.js @@ -85,14 +85,34 @@ describe('chat - OpenCode', async () => { assert.equal(result, 'hello world'); }); - it('calls process.exit(1) when OpenCode fails', async () => { + it('sets a request timeout on OpenCode calls so a stalled server fails fast', async () => { + process.env.OPENCODE_BASE_URL = 'http://opencode.local:4096'; + const timeouts = []; + mock.method(axios, 'post', async (url, _payload, opts) => { + timeouts.push(opts.timeout); + if (url.endsWith('/session')) return { data: { id: 'ses_test' } }; + return { data: { parts: [{ type: 'text', text: 'ok' }] } }; + }); + + await chat('sys', 'user'); + + assert.equal(timeouts.length, 2); + for (const t of timeouts) { + assert.equal(typeof t, 'number'); + assert.ok(t > 0, 'OpenCode 請求必須帶正數 timeout,避免無限等待'); + } + }); + + it('throws (does not process.exit) when OpenCode fails, so callers can degrade gracefully', async () => { process.env.OPENCODE_BASE_URL = 'http://opencode.local:4096'; mock.method(axios, 'post', async () => { throw new Error('fail'); }); const exitMock = mock.method(process, 'exit', () => { throw new Error('exit:1'); }); - await assert.rejects(() => chat('sys', 'user'), /exit:1/); + // 失敗時向外拋出原始錯誤,而非 process.exit(1),讓 Step5 的 Promise.allSettled + // 與去重/過濾的 try/catch fallback 能各自處理,不會整個流程被砍掉。 + await assert.rejects(() => chat('sys', 'user'), /fail/); - assert.equal(exitMock.mock.calls[0].arguments[0], 1); + assert.equal(exitMock.mock.calls.length, 0, 'chat 不應再呼叫 process.exit'); }); });